Я наткнулся на стиль кнопки, который мне понравился, и хотел воссоздать его для использования в pyqt5, используя класс QPushButton и стилизовав его с помощью css. Если я смогу воссоздать его, я смогу круто изменить его цвета.
С точки зрения CSS, кнопка имеет «сплошную» рамку толщиной 1 пиксель со всех сторон и «радиус границы» 2 пикселя, и я нашел ее высотой 21 пиксель, однако, очевидно, ее размер можно изменить. Если кому-то интересно, это скромная кнопка «Vista»:
Платформа Qt реализует функции, основанные на старом стандарте CSS 2.1, поэтому любой, кто знаком только с CSS, может знать, как это делается.
К сожалению, моя неопытность в CSS меня подводит. Обычно я могу решить проблему методом грубой силы, а затем привести в порядок и оптимизировать свою реализацию, однако здесь я попал в тупик.
90% внешнего вида кнопки можно создать с помощью простого линейного градиента цвета фона, однако существует «внутренняя граница» (эффект скоса) толщиной 1 пиксель, которая находится на одном уровне с реальной границей и проходит по всему периметру. кнопку, у меня возникли проблемы с воссозданием этого вида.
Фаска обманчиво имеет один цвет внизу и другой вверху, а по бокам кнопки есть отдельный линейный градиент, связывающий их вместе.
Я пробовал экспериментировать со стилями границ, даже используя градиентные цвета для границы, но безуспешно. То, что казалось полезным, под названием «двойной» стиль границы, к сожалению, мне не пригодилось, поскольку вы не можете очень хорошо управлять обоими цветами границ по отдельности.
Ниже должно все это объяснить.
Никакой линейный градиент цвета фона не даст вам этого:
Простой линейный градиент с несколькими остановками дает следующее:
Сложный (грубо сделанный) линейный градиент может иметь скос сверху+снизу:
А это настоящая кнопка (сделанная не мной), обратите внимание на боковые стороны:
Вот код для третьего изображения (переработанный линейный градиент с очень большим количеством остановок, не смейтесь, это грубая версия — ее можно сделать с меньшим количеством шагов, но она идеальна по пикселям везде, кроме сторон)
import sys
from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QVBoxLayout)
app = QApplication([])
window_container = QWidget()
window_container.setWindowTitle("Buttons")
window_container.setMinimumSize(200,125)
window_container.setObjectName("window_container")
window_container.setStyleSheet("QPushButton {"
+ "border-style: solid;"
+ "border-width: 1px 1px 1px 1px;"
+ "border-radius: 2px;"
+ "border-color: #707070;"
+ "font: 9;"
+ "padding: 3px;"
+ "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, "
+ "stop:0 #FCFCFC, "
+ "stop:0.075 #FCFCFC, stop:0.111 #F2F2F2, stop:0.216 #F2F2F2, "
+ "stop:0.217 #F1F1F1, stop:0.252 #F0F0F0, stop:0.320 #EFEFEF, "
+ "stop:0.350 #EDEDED, stop:0.389 #ECECEC, stop:0.499 #EBEBEB, "
+ "stop:0.500 #DDDDDD, stop:0.556 #DBDBDB, stop:0.611 #DADADA, "
+ "stop:0.667 #D8D8D8, stop:0.700 #D6D6D6, stop:0.750 #D4D4D4, "
+ "stop:0.800 #D2D2D2, stop:0.870 #D1D1D1, stop:0.910 #CFCFCF, "
+ "stop:0.945 #F3F3F3, "
+ "stop:1.0 #F3F3F3);"
+ "}"
)
button_1 = QPushButton("Top")
layout = QVBoxLayout()
layout.addWidget(button_1)
window_container.setLayout(layout)
window_container.show()
sys.exit(app.exec())
После полного анализа могу предоставить разбивку того, что нужно сделать, извините за грубые схемы:
На этой диаграмме (c) — основная градиентная заливка (которая не имеет общих цветов с чем-либо еще).
Вот легенда к приведенной выше диаграмме, которая поможет разобраться:
Я не знаю, достиг ли я пределов возможностей инструмента линейного градиента, или мне не хватает чего-то очень простого, но у меня есть все основания полагать, что это возможно, просто это довольно специфично.
Настоящая хитрость здесь в том, что «боковые» скосы должны представлять собой линейные градиенты, соединяющие верхний ряд пикселей внутри кнопки с нижним рядом, я не могу этого понять.
Возможно, кому-то это покажется развлечением или вызовом, я рад попробовать что угодно (в пределах разумного).
Спасибо за доработку, теперь намного лучше. Вот некоторые примечания. 1. setStyleSheet()
не полностью переопределяет setStyle()
; фактически, setStyleSheet()
создает внутренний QStyleSheetStyle для всех задействованных виджетов, но по-прежнему в некоторой степени использует набор QStyle (для виджета или всего приложения), поэтому обычно рекомендуется использовать QApplication.setStyle('fusion')
, чтобы получить согласованное/надежное поведение QSS . 2. Хотя я понимаю необходимость отступов и аккуратности, я бы посоветовал вам использовать тройные кавычки при написании QSS, поскольку это облегчает их чтение и редактирование.
3. Боюсь, что для вашей проблемы нет немедленного решения, по крайней мере, с помощью только градиентов QSS; основная причина в том, что Qt ограничен возможностями, предоставляемыми его градиентами, и нет возможности их «расширить» (родительский класс QGradient предоставляется для предоставления общего типа и API, а не для создания подклассов альтернативных градиентов); Другая причина заключается в том, что QSS допускает градиенты только с относительными координатами (0,0–1,0), а это означает, что попытка жестко закодировать «вычисляемые по пикселям» остановки будет эффективна только для очень конкретных размеров.
То, чего вы хотите достичь, невозможно сделать исключительно с помощью градиентов QSS (таблицы стилей Qt):
Значит, это невозможно? Не полностью.
Исторически Qt в некоторых случаях использовал стили на основе растровых изображений (особенно в Windows, по крайней мере, до тех пор, пока они не использовали «QWindowsVistaStyle» для Windows <= 10, я еще не знаю, как работает QWindows11Style, представленный в Qt6.7).
Решение, таким образом, относительно простое: используйте растровое изображение, взятое из «настоящей кнопки», в качестве свойства border-image . Документация по его Border Image объясняет (немного неясно), как оно работает:
Рамочное изображение — это изображение, состоящее из девяти частей (слева вверху, в центре вверху, справа вверху, слева в центре, в центре, справа в центре, слева внизу, в центре внизу и справа внизу). Если требуется граница определенного размера, угловые части используются как есть, а верхняя, правая, нижняя и левая части растягиваются или повторяются, чтобы получить границу нужного размера.
Подробности смотрите в Черновик спецификации CSS3.
Note: this is one of the few cases for which QSS use a CSS3 based implementation, while it normally follows the CSS2.1 specification only.
Это также объясняется в разделе QPushButton и изображения документации «Пример таблиц стилей Qt». Расширенное объяснение можно найти в документации Mozilla CSS, его можно упростить следующим образом:
Свойство border-image
будет содержать URL-адрес изображения и значения, указывающие размер (в пикселях) верхнего, правого, нижнего и левого краев. Поскольку в таком простом случае углы на самом деле являются квадратами, мы можем использовать сокращенный синтаксис; добавление одного значения автоматически распространит это значение на все углы, а это означает, что это значение будет использоваться как для высоты, так и для ширины каждого угла.
Единственное, что нам осталось, — это получить правильное исходное изображение для кнопки. Если у вас есть доступ к установке Qt в системе Windows с использованием предполагаемого стиля («windowsvista»), это относительно просто:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
app = QApplication([])
btn = QPushButton()
btn.resize(200, 200)
pm = QPixmap(b.size())
pm.fill(Qt.transparent)
# by default render() also draws the background, let's explicitly avoid that
btn.render(pm, flags=QWidget.RenderFlag(0))
pm.save('button.png')
Теперь у нас есть изображение кнопки размером 200x200 (обратите внимание, что в стиле Windows Vista кнопки обычно имеют отступ в 1 пиксель по краям).
Давайте откроем его в программе для редактирования изображений.
Нам нужно кое-что скопировать/вставить/изменить размер, чтобы получить подходящее изображение: стиль, похоже, не использует горизонтальные градиенты, поэтому мы можем вырезать часть вертикального центра, оставив достаточно пикселей для углов. border-image
и фактическое горизонтальное содержимое (области 5 и 7); вертикальный размер остается неизменным, поскольку на самом деле он содержит некоторые данные о градиенте.
Учитывая, что углы на самом деле требуют 4x4 пикселей, результатом будет изображение размером 10x200: оно имеет углы 4x4, верхнюю/нижнюю стороны 2x4 и левую/правую стороны 2x192, а также содержит внутреннюю белую рамку:
Теперь мы просто используем правильный синтаксис QSS, учитывая вышеизложенное:
window_container.setStyleSheet('''
QPushButton {
border-image: url("btn.png") 3;
border: 3px solid transparent;
}
''')
Мы все? Еще не совсем.
При стилизации интерактивных виджетов нам необходимо учитывать их состояния. В частности, кнопка обычно имеет следующие состояния и их комбинации:
В зависимости от стиля, настройки кнопки и взаимодействия с пользователем кнопка может быть, например, не нажатой и сфокусированной (нажатие левой кнопки мыши и отход от кнопки, пока кнопка мыши все еще нажата). Это означает, что нам нужно создать связанные QPixmap для всех этих комбинаций и связанных правил в QSS.
Хотя создание правил QSS для вышеперечисленного в любом случае будет обязательным, создание этих растровых изображений может оказаться немного более сложным.
Хотя одной из возможностей является создание отдельных копий исходного растрового изображения и изменение их внешнего вида, если у вас есть доступ к системе Windows, как указано выше, теоретически это проще: к счастью, QStyle реализован таким образом, что ему не нужны настоящие виджеты в чтобы раскрасить свою внешность.
Я не могу предоставить полный код, но в качестве основы рассмотрю следующее:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
app = QApplication([])
opt = QStyleOptionButton()
opt.palette(app.palette())
opt.rect = QRect(0, 0, 100, 200)
style = app.style()
pm = QPixmap(opt.rect.size())
pm.fill(Qt.transparent)
qp = QPainter(pm)
style.drawControl(style.CE_PushButton, opt, qp)
qp.end()
pm.save('btn-disabled-off.png')
pm.fill(Qt.transparent)
qp.begin(pm)
opt.state |= style.State_Enabled
style.drawControl(style.CE_PushButton, opt, qp)
qp.end()
pm.save('btn-enabled-off.png')
...
Note that I used an arbitrary width of 100 pixels: under a certain value (I don't know which), the left border is missing the white line. Using a width of 100 will require more image memory, I'll leave it to you to find the best approximation that also avoids unnecessary widths. By having proper mastering of QPainter capabilities you can apply dynamic cropping and save more memory friendly images.
В действительности, вам следует реализовать вышеизложенное с помощью возможных комбинаций, которые учитывают все следующие флаги QStyle.StateFlag:
State_Active
(для активного окна)State_HasFocus
State_MouseOver
State_Sunken
(при нажатии и под мышкой, если она нажата)State_Raised
(теоретически не требуется, если только не используется свойство Flat, что также потребует дальнейшей итерации для переключения option.features
на QStyleOptionButton.None
и QStyleOptionButton.Flat
)В итоге получится от 8 до 16 возможных комбинаций изображений и соответствующих правил QSS (по крайней мере).
Выполнение вышеописанного может быть довольно раздражающим, но не думайте, что правильная реализация градиентов QSS для всех этих состояний (при условии, что они бы работали) была бы проще, особенно учитывая неизбежную проблему градиентов на основе пикселей.
Спасибо @musicamante за время и усилия, я дам правильный и полный ответ, как только вернусь сегодня с работы.
Еще раз привет @musicamante, это невероятно полезно, именно то, что мне нужно. Мне нужно проработать множество мелких деталей, но это не проблема... За исключением внутреннего скоса, «настоящая» кнопка Vista использует только 16 различных цветов для горизонтального градиента, это постоянно, поэтому шаг происходит, когда кнопка абсурдно высока, ваш пример отлично подходит для «настоящего отдыха». При желании я могу выдолбить «png», такой как ваш, оставив только границы, и заполнить внутреннюю часть bg-цветом (градиентом), что, по сути, позволяет создавать кнопки «hd» без наступления. Должен ли я принять ваш ответ? Нужно ли мне обновлять ОП? Спасибо
@ bfh47 bfh47 ну, есть еще одна альтернатива, но она полностью предотвращает использование QSS (по крайней мере, для всего, что связано с рисованием кнопок, кроме отображения текста): используйте QProxyStyle. Если вы достаточно знаете цвета, используемые кнопками Vista, вы можете напрямую использовать функции QPainter (в качестве пера внешней границы можно установить QBrush с линейным градиентом). Это немного сложнее и менее прямолинейно, чем использование коллекции растровых изображений, но это также позволит реализовать и другие аспекты (возможно, включая анимацию).
@ bfh47 Если вы планируете использовать изображение рамки только для рамки, вам все равно нужно будет создать возможные комбинации (нажата/не нажата, включена/выключена и, возможно, сфокусирована/ранее нажата): проведите несколько тестов, чтобы увидеть, какие из них являются на самом деле необходимо. Как только это будет сделано, вы можете создать дополнительные отдельные селекторы только с цветами фона, используя небольшой трюк: padding: -2px;
и background-clip: content;
, чтобы отступы расширялись напротив размера границы, но не отображались за пределами изображения границы. Впрочем, решать вам, я считаю, что мой ответ уже достаточно :-)
@musicmante, Вы правы, в долгосрочной перспективе для любого большого проекта необходимость в png-это действительно не «способ делать что-то», я только недавно начал учиться, я поставил себе первоначальную задачу «создать стиль» чего-то с нуля вверх. Я даже наткнулся на этот метод «QProxyStyle» (настоящий стиль), но он выглядел более устрашающим и менее «доступным», поэтому я решил сначала изучить маршрут css. CSS, хотя и более доступен, на самом деле довольно быстро становится довольно сложным, так что в какой-то момент я могу наткнуться на стену.
@musicmante Мое мышление таково: «Сначала сделай что-нибудь, а потом ищи способ сделать то же самое лучше», я думаю, это называется «принцип быстрой неудачи», но «быстрая» часть редко бывает быстрой для меня, ха-ха. Выбор CSS для оформления тем (в настольном приложении) на самом деле является обязательством, но я не могу выбрать путь, пока не знаю некоторых подробностей о том, как будет выглядеть каждый путь. Этот вопрос, безусловно, был для меня хорошим обучающим моментом, все, что осталось сейчас в CSS, - это «варианты», такие как фокус/наведение мыши и анимация - примеры которых я видел, но это выглядит неинтересно :) Я собираюсь примите ваш ответ, еще раз спасибо!
QSS возможны. Чрезвычайно полезно легко изменять аспекты выделенных виджетов, это может обеспечить хороший уровень настройки, но также весьма раздражает правильная настройка сложных виджетов (полосы прокрутки, поля прокрутки и т. д.), поскольку после установки одного свойства подэлемента управления необходимо установить все остальное. также. Его можно использовать во всем приложении, но, вероятно, не рекомендуется для больших программ, которые также должны использовать стандартные виджеты (последовательность может стать проблемой), если вы не готовы написать правила для всех типов виджетов. Некоторое время я часто ими пользовался, прежде чем осознал: 1. их пределы, »
» после понимания того, как они на самом деле работают (помогает изучение исходников Qt); 2. что в какой-то момент найти способ получить с их помощью конкретные результаты иногда было досадно сложно, тогда как правильное использование функций QStyle на самом деле было лучше, относительно немедленным и давало более точный контроль над результатом. Однако изучить QStyle непросто и не быстро: вам нужно иметь достаточный опыт работы со многими сопутствующими аспектами (отображение шрифтов, управление геометрией, надлежащее знание возможностей каждого виджета и т. д.), которые QSS делает самостоятельно, и вот почему они являются хорошим выбором для новичков.
PS/OT: @username
должен вызывать всплывающее окно завершения табуляции, всегда следуйте ему как можно чаще, иначе вы можете получить ненужные «теги»; в этом также нет необходимости, когда целью является ОП (задающий вопрос или отвечающий) или участвует только один другой пользователь. Меня уведомили о ваших комментариях только из-за того, как работает система, но иначе я бы не сделал этого из-за опечаток: мой идентификатор «musicamante», а не «musicmante» ;-)
Еще раз здравствуйте, @musicamante, извините за ошибку в написании вашего имени! И... Раньше я видел всплывающее окно автозаполнения имен, но по какой-то причине оно больше не происходит в моем браузере :( У меня только Firefox ESR 115 (и только с ublock, и его отключение не имело никакого значения). Вскоре после этого ответа я создал грубый генератор изображений границ, используя cv2, который создает детализированное изображение границы на основе любого 24-битного цвета, потому что я все еще нахожусь на этапе «выяснения того, как я хочу, чтобы все выглядело». Просто хотел сказать спасибо. снова. Однажды я могу отказаться от CSS, но все равно во всем разбираюсь :)
@musicamante, еще раз здравствуйте, я сделал, как вы просили (удалил предыдущие комментарии и сильно подчистил исходный пост. Не знаю, почему я не сделал все так просто с самого начала). Кстати, как только вы напишете «setstylesheet», затронутые элементы потеряют все стили из «setStyle» просто потому, что они не могут работать вместе, надеюсь, это поможет. Вы тоже можете удалить свои предыдущие комментарии, чтобы уменьшить беспорядок. Теперь, если бы я мог получить встроенные изображения, это могло бы превратиться в вопрос «ОК», действительно достойный чьего-то времени :)