Related
Starting the program, the QIcon is aligned on the left (it's standard i guess) with the text right to it.
Instead I want the icon to be centered on top with the text below it.
I tried using setStyleSheet with show_all.setStyleSheet("QIcon { vertical-align: top }") and show_all.setStyleSheet("QPushButton { text-align: bottom }").
How can I achieve this?
QPushButton doesn't allow to choose the layout of its icon and label. Also, remember that while Qt features style sheets to style widgets, not all CSS known properties and selectors are available. Furthermore, style sheets only work on widgets, so using the QIcon selector isn't supported, since QIcon is not a QWidget subclass.
The most simple solution is to use a QToolButton and set the toolButtonStyle correctly:
self.someButton = QtWidgets.QToolButton()
# ...
self.someButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
The alternative is to subclass the button, provide a customized paint method and reimplement both sizeHint() and paintEvent(); the first is to ensure that the button is able to resize itself whenever required, while the second is to paint the button control (without text!) and then paint both the icon and the text.
Here's a possible implementation:
from PyQt5 import QtCore, QtGui, QtWidgets
class CustomButton(QtWidgets.QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._icon = self.icon()
if not self._icon.isNull():
super().setIcon(QtGui.QIcon())
def sizeHint(self):
hint = super().sizeHint()
if not self.text() or self._icon.isNull():
return hint
style = self.style()
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
margin = style.pixelMetric(style.PM_ButtonMargin, opt, self)
spacing = style.pixelMetric(style.PM_LayoutVerticalSpacing, opt, self)
# get the possible rect required for the current label
labelRect = self.fontMetrics().boundingRect(
0, 0, 5000, 5000, QtCore.Qt.TextShowMnemonic, self.text())
iconHeight = self.iconSize().height()
height = iconHeight + spacing + labelRect.height() + margin * 2
if height > hint.height():
hint.setHeight(height)
return hint
def setIcon(self, icon):
# setting an icon might change the horizontal hint, so we need to use a
# "local" reference for the actual icon and go on by letting Qt to *think*
# that it doesn't have an icon;
if icon == self._icon:
return
self._icon = icon
self.updateGeometry()
def paintEvent(self, event):
if self._icon.isNull() or not self.text():
super().paintEvent(event)
return
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
opt.text = ''
qp = QtWidgets.QStylePainter(self)
# draw the button without any text or icon
qp.drawControl(QtWidgets.QStyle.CE_PushButton, opt)
rect = self.rect()
style = self.style()
margin = style.pixelMetric(style.PM_ButtonMargin, opt, self)
iconSize = self.iconSize()
iconRect = QtCore.QRect((rect.width() - iconSize.width()) / 2, margin,
iconSize.width(), iconSize.height())
if self.underMouse():
state = QtGui.QIcon.Active
elif self.isEnabled():
state = QtGui.QIcon.Normal
else:
state = QtGui.QIcon.Disabled
qp.drawPixmap(iconRect, self._icon.pixmap(iconSize, state))
spacing = style.pixelMetric(style.PM_LayoutVerticalSpacing, opt, self)
labelRect = QtCore.QRect(rect)
labelRect.setTop(iconRect.bottom() + spacing)
qp.drawText(labelRect,
QtCore.Qt.TextShowMnemonic|QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop,
self.text())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = CustomButton('Alles anzeigen', icon=QtGui.QIcon.fromTheme('document-new'))
w.setIconSize(QtCore.QSize(32, 32))
w.show()
sys.exit(app.exec_())
Alternatively, try it:
import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (QApplication, QWidget, QGridLayout,
QToolBar, QAction)
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
add_action = QAction(QIcon("img/add.png"), "Add", self)
add_action.triggered.connect(self.addValue)
sub_action = QAction(QIcon("img/min.png"), "Sub", self)
sub_action.triggered.connect(self.subValue)
toolbar = QToolBar()
toolbar.setContentsMargins(0, 0, 0, 0)
toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon | Qt.AlignLeading)
toolbar.setIconSize(QSize(50, 50))
toolbar.addAction(add_action)
toolbar.addAction(sub_action)
rootGrid = QGridLayout(self)
rootGrid.addWidget(toolbar)
def addValue(self):
print("def addValue:")
def subValue(self):
print("def subValue:")
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Widget()
main.show()
sys.exit(app.exec_())
I'm currently creating a to-do application. I have a side menu (which is just QPushButtons in a vbox) and have a main window widget to show content. However, I need a way to show different content in the main widget based on what side menu button is pressed. I have tried to use QStackedLayout, but I don't like the way it closes the main window and switches to a new one. I've also tried to use QTabWidget, but the tabs are at the top. Is there a way to sub-class QTabWidget and create a custom QTabWidget with the tab buttons on the side? If not, is there a way to do this? The image above is what I have so far.
This is all my code:
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
from datetime import date
import sys
months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November",
"December"]
stylesheet = """
QWidget{
background-color: white;
}
QWidget#sideMenuBackground{
background-color: #f7f7f7;
}
QVBoxLayout#sideMenuLayout{
background-color: grey;
}
QPushButton#sideMenuButton{
text-align: left;
border: none;
background-color: #f7f7f7;
max-width: 10em;
font: 16px;
padding: 6px;
}
QPushButton#sideMenuButton:hover{
font: 18px;
}
QLabel#today_label{
font: 25px;
max-width: 70px;
}
QLabel#todays_date_label{
font: 11px;
color: grey;
}
QPushButton#addTodoEventButton{
border: none;
max-width: 130px;
}
"""
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("To-Do Application")
self.setGeometry(200, 200, 800, 500)
self.initUI()
def initUI(self):
self.nextWeekPage = QtWidgets.QLabel()
backgroundWidget = QtWidgets.QWidget()
backgroundWidget.setObjectName("sideMenuBackground")
backgroundWidget.setFixedWidth(150)
layout = QtWidgets.QHBoxLayout()
layout.addWidget(backgroundWidget)
sideMenuLayout = QtWidgets.QVBoxLayout()
sideMenuLayout.setObjectName("sideMenuLayout")
taskLayout = QtWidgets.QVBoxLayout()
backgroundWidget.setLayout(sideMenuLayout)
layout.addLayout(taskLayout)
self.setSideMenu(sideMenuLayout)
sideMenuLayout.addStretch(0)
self.setMainLayout(taskLayout)
taskLayout.addStretch(0)
mainWidget = QtWidgets.QWidget()
mainWidget.setLayout(layout)
self.setCentralWidget(mainWidget)
def setSideMenu(self, layout):
self.todayButton = QtWidgets.QPushButton(" Today")
self.nextWeekButton = QtWidgets.QPushButton("Next 7 Days")
self.calendarButton = QtWidgets.QPushButton("Calendar")
sideMenuButtons = [self.todayButton, self.nextWeekButton, self.calendarButton]
for button in sideMenuButtons:
button.setObjectName("sideMenuButton")
layout.addWidget(button)
sideMenuButtons[0].setIcon(QtGui.QIcon("today icon.png"))
sideMenuButtons[1].setIcon(QtGui.QIcon("week icon.png"))
sideMenuButtons[2].setIcon(QtGui.QIcon("calendar icon.png"))
sideMenuButtons[0].pressed.connect(self.todayButtonPress)
sideMenuButtons[1].pressed.connect(self.nextWeekButtonPress)
sideMenuButtons[2].pressed.connect(self.calendarButtonPress)
def setMainLayout(self, layout):
today_label_widget = QtWidgets.QWidget()
today_label_layout = QtWidgets.QHBoxLayout()
layout.addWidget(today_label_widget)
today_label_widget.setLayout(today_label_layout)
month = date.today().month
day = date.today().day
today = f"{months[month - 1]}{day}"
self.todays_date = QtWidgets.QLabel(today)
self.todays_date.setObjectName("todays_date_label")
self.today_label = QtWidgets.QLabel("Today")
self.today_label.setObjectName("today_label")
self.addTodoEventButton = QtWidgets.QPushButton()
self.addTodoEventButton.setObjectName("addTodoEventButton")
self.addTodoEventButton.setIcon(QtGui.QIcon("add event button.png"))
self.addTodoEventButton.setToolTip("Add To Do Event")
today_label_layout.addWidget(self.today_label)
today_label_layout.addWidget(self.todays_date)
today_label_layout.addWidget(self.addTodoEventButton)
self.labels = ["button1", "button2", "button3", "button4", "Button5"]
for today_events in self.labels:
label = QtWidgets.QLabel(today_events)
layout.addWidget(label)
def addTodoEvent(self):
pass
def todayButtonPress(self):
print("today button pressed")
def nextWeekButtonPress(self):
print("Next week button pressed")
def calendarButtonPress(self):
print("calendar button pressed")
def main():
app = QtWidgets.QApplication(sys.argv)
app.setStyleSheet(stylesheet)
window = MainWindow()
window.show()
app.exec_()
if __name__ == "__main__":
main()
Using a stacked layout shouldn't open a new window when used correctly. The snippet below outlines how the original code could be adapted to use a stacked layout to open different pages in the same window.
class MainWindow(QtWidgets.QMainWindow):
def initUI(self):
# same as before
self.taskLayout = QtWidgets.QStackedLayout()
self.setMainLayout(self.taskLayout)
# same as before
def setMainLayout(self, layout)
today = self.todayWidget()
next_week = self.nextWeekWidget()
calendar_widget = self.calendarWidget()
layout.addWidget(today)
layout.addWidget(next_week)
layout.addWidget(calendar_widget)
def todayWidget(self)
widget = QtWidgets.QWidget(self)
layout = QVBoxLayout(widget)
# setup layout for today's widget
return widget
def nextWeekWidget(self)
widget = QtWidgets.QWidget(self)
layout = QVBoxLayout(widget)
# setup layout for next week's widget
return widget
def calendarWidget(self)
widget = QtWidgets.QWidget(self)
layout = QVBoxLayout(widget)
# setup layout for calendar widget
return widget
def todayButtonPress(self):
self.taskLayout.setCurrentIndex(0)
def nextWeekButtonPress(self):
self.taskLayout.setCurrentIndex(1)
def calendarButtonPress(self):
self.taskLayout.setCurrentIndex(2)
Here is a solution using a custom QListWidget combined with a QStackedWidget.
QListWidget on the left and QStackedWidget on the right, then add a QWidget to it in turn.
When adding a QWidget on the right, there are two variants:
The list on the left is indexed according to the serial number. When adding a widget, the variable name with the serial number is indicated on the right, for example, widget_0, widget_1, widget_2, etc., so that it can be directly associated with the serial number QListWidget.
When an item is added to the list on the left side, the value of the corresponding widget variable is indicated on the right.
from random import randint
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (QWidget, QListWidget, QStackedWidget,
QHBoxLayout, QListWidgetItem, QLabel)
class LeftTabWidget(QWidget):
def __init__(self, *args, **kwargs):
super(LeftTabWidget, self).__init__(*args, **kwargs)
self.resize(800, 600)
# Left and right layout (one QListWidget on the left + QStackedWidget on the right)
layout = QHBoxLayout(self, spacing=0)
layout.setContentsMargins(0, 0, 0, 0)
# List on the left
self.listWidget = QListWidget(self)
layout.addWidget(self.listWidget)
# Cascading window on the right
self.stackedWidget = QStackedWidget(self)
layout.addWidget(self.stackedWidget)
self.initUi()
def initUi(self):
# Initialization interface
# Switch the sequence number in QStackedWidget by the current item change of QListWidget
self.listWidget.currentRowChanged.connect(
self.stackedWidget.setCurrentIndex)
# Remove the border
self.listWidget.setFrameShape(QListWidget.NoFrame)
# Hide scroll bar
self.listWidget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.listWidget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# Here we use the general text with the icon mode (you can also use Icon mode, setViewMode directly)
for i in range(5):
item = QListWidgetItem(
QIcon('Ok.png'), str('Option %s' % i), self.listWidget)
# Set the default width and height of the item (only height is useful here)
item.setSizeHint(QSize(16777215, 60))
# Text centered
item.setTextAlignment(Qt.AlignCenter)
# Simulate 5 right-side pages (it won't loop with the top)
for i in range(5):
label = QLabel('This is the page %d' % i, self)
label.setAlignment(Qt.AlignCenter)
# Set the background color of the label (randomly here)
# Added a margin margin here (to easily distinguish between QStackedWidget and QLabel colors)
label.setStyleSheet('background: rgb(%d, %d, %d); margin: 50px;' % (
randint(0, 255), randint(0, 255), randint(0, 255)))
self.stackedWidget.addWidget(label)
# style sheet
Stylesheet = """
QListWidget, QListView, QTreeWidget, QTreeView {
outline: 0px;
}
QListWidget {
min-width: 120px;
max-width: 120px;
color: white;
background: black;
}
QListWidget::item:selected {
background: rgb(52, 52, 52);
border-left: 2px solid rgb(9, 187, 7);
}
HistoryPanel::item:hover {background: rgb(52, 52, 52);}
QStackedWidget {background: rgb(30, 30, 30);}
QLabel {color: white;}
"""
if __name__ == '__main__':
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
app.setStyleSheet(Stylesheet)
w = LeftTabWidget()
w.show()
sys.exit(app.exec_())
I ran the below MCVE snippet on two different systems which produced the windows depicted in the screenshots. As you can see, their border styles differ. I do not mind that. The border styles are allowed to differ on the two systems. However, for one system, I'd like the border style of both QtWidgets to be the same. The border style of QLabel doesn't seem to be set, yet.
How can I set the border style of the QLabel to the same border style as the QTreeWidget?
MCVE snippet
import sys
from PyQt5.QtWidgets import *
class widget(QWidget):
def __init__(self):
super().__init__()
treewidget = QTreeWidget(self)
label = QLabel(self)
label.setStyleSheet("background-color: white; min-height: 200px;")
grid = QGridLayout()
grid.setSpacing(10)
grid.addWidget(treewidget, 1, 0)
grid.addWidget(label, 2, 0)
self.setLayout(grid)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
f = widget()
sys.exit(app.exec_())
Screenshot from two different systems
Your general question is impossible to solve for the following reasons:
The properties of a widget are not shared by other widgets.
The values of certain properties have default values and are highly dependent on the decision of the Qt developers, such as the edge of QLabel, that is, the edge can be modified but the default value can not be read.
Not all visual elements can be modified with Qt Style Sheet, in reality it is very limited, it is often better to create a customized QStyle.
But your particular problem can be solved, in this case we set the border to none.
from PyQt5 import QtWidgets
QSS = '''
QTreeWidget{
border: None
}
QLabel{
background-color: white;
min-height: 200px;
}
'''
class widget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
treewidget = QtWidgets.QTreeWidget()
label = QtWidgets.QLabel()
grid = QtWidgets.QGridLayout(self)
grid.setSpacing(10)
grid.addWidget(treewidget, 1, 0)
grid.addWidget(label, 2, 0)
self.show()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyleSheet(QSS)
f = widget()
sys.exit(app.exec_())
Good morning.
Question 1
In my code I am trying to make it so when you click on a button, it flashes red for a few milliseconds.
As you can see, I have a fade and unfade function. How can the fade function know it has to change the StyleSheet of the button that's calling the function?
from PyQt5.QtCore import (Qt, QTimer)
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider, QVBoxLayout, QApplication, QPushButton, QGridLayout, QHBoxLayout, QLabel)
from PyQt5.QtGui import (QFont, QPainter, QColor)
# Font declaration
mainFont = QFont("arial", 18, QFont.Bold)
# Color declaration
"""
backgroundColor = "#2e3436"
buttonColor = "#729fcf"
textColor = "#2e3436"
greenColor = "#27bc10"
redColor = "#d81711"
"""
backgroundColor = "#2e3436"
buttonColor = "#ffffff"
textColor = "#000000"
greenColor = "#27bc10"
redColor = "#d81711"
# main buttons size
mainWidth = 160
mainHeight = 80
# Variable declaration
CurrentSpeed = 1
class MainInterface(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
b1 = QPushButton('Start')
b1.setFixedWidth(mainWidth)
b1.setFixedHeight(mainHeight)
b1.setStyleSheet("background-color: %s; color: %s" % (greenColor, textColor))
b1.setFont(mainFont)
b1.clicked.connect(self.fade)
b2 = QPushButton('Stop')
b2.setFixedWidth(mainWidth)
b2.setFixedHeight(mainHeight)
b2.setStyleSheet("background-color: %s; color: %s" % (redColor, textColor))
b2.setFont(mainFont)
b3 = QPushButton('Speed -')
b3.setFixedWidth(mainWidth)
b3.setFixedHeight(mainHeight)
b3.setStyleSheet("background-color: %s; color: %s" % (buttonColor, textColor))
b3.setFont(mainFont)
l1 = QLabel(str(CurrentSpeed))
l1.setStyleSheet("color: white; margin: 15px; border: 2px solid white")
l1.setFont(mainFont)
l1.setAlignment(Qt.AlignCenter)
b4 = QPushButton('Speed +')
b4.setFixedWidth(mainWidth)
b4.setFixedHeight(mainHeight)
b4.setStyleSheet("background-color: %s; color: %s" % (buttonColor, textColor))
b4.setFont(mainFont)
grid = QGridLayout()
grid.setColumnMinimumWidth(50, 400)
grid.setRowMinimumHeight(10, 250)
grid.addWidget(b1, 0, 0)
grid.addWidget(b2, 0, 2)
grid.addWidget(b3, 1, 0)
grid.addWidget(l1, 1, 1)
grid.addWidget(b4, 1, 2)
self.setStyleSheet("background-color: %s" % backgroundColor)
self.setLayout(grid)
self.setGeometry(300, 200, 850, 450)
self.setWindowTitle('Signal & slot')
self.show()
def fade(self, button):
button.setWindowOpacity(0.5)
button.setStyleSheet("background-color: red")
QTimer.singleShot(300, self.unfade)
def unfade(self):
button.setWindowOpacity(1)
button.setStyleSheet("background-color: #2e3436")
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = MainInterface()
sys.exit(app.exec_())
Question 2
More advanced, how would I make an animation on the button similar to when you click on the lockscreen of your phone?
Thanks in advance.
First, slots can call self.sender() to find out object that emitted signal.
Next, you have to be mindful of the signal payload, which in this case is an optional "checked True/False", so button is not a valid parameter for fade() slot.
It is also advisable to decorate slots with pyqtSlot, there is no disadvantage and even if its advantages are not relevant now, they will be later (and save you some bugs related to signal handling when decorator not used, as posted on mailing list). As pointed out by ehkumoro, since the checked is optional and you don't use it, it is sufficient to decorate with pyqtSlot().
Finally, unfade() needs to know which button to unfade; a lambda as done by eyllanesc is fine but I'll show how to do it with data member, which has the advantage that it plays role of a "state flag" on MainInterface. I.e. when not None, it indicates that MainInterface widget is "currently fading a button"; this often comes in handy when your widget responds to an event that occurs between end of fade() and start of unfade(), like another click (widget needs to test if it is already fading another button).
def __init__(self):
...
self.__fading_button = None
#pyqtSlot()
def fade(self):
self.__fading_button = self.sender() # enter the "fading button" state
self.__fading_button.setWindowOpacity(0.5)
self.__fading_button.setStyleSheet("background-color: red")
QTimer.singleShot(300, self.unfade)
def unfade(self):
self.__fading_button.setWindowOpacity(1)
self.__fading_button.setStyleSheet("background-color: #2e3436")
self.__fading_button = None # exit the "fading button" state
I want to add a popup menu to QPushButton, but only popup it when you click near the arrow, if you click other area on the button, it calls the slot connected in main UI.
I know there is QToolButton, and you can set its ToolButtonPopupMode to MenuButtonPopup, but for some reason it looks different than then rest of the button on my UI, I assume I could somehow modify the style of it to make it look exactly like QPushButton, anyway in the end I decided to subclass QPushButton instead.
The problems in the following code are:
1. How do I get the rect of the arrow, maybe show a dashed rect around the arrow, I thought the "popup menu hotspot" area should be a little bit bigger than the arrow. right now I hardcoded 20px, but I think it should be retrieved from QStyle?
[solved] How to make the button look "pressed" when clicked not near the arrow, right now its look does not change, I guess it's because I did not call base class MousePressEvent, because I don't want the menu to popup when clicked elsewhere.
How to move the position of the arrow, in my applicaton it is too close to the right edge, how can I move it to the left a little bit?
code:
from PyQt4 import QtGui, QtCore
import sys
class MyButton(QtGui.QPushButton):
def __init__(self, parent=None):
super(MyButton, self).__init__(parent)
def mousePressEvent(self, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
# figure out press location
pos = event.pos
topRight = self.rect().topRight()
bottomRight = self.rect().bottomRight()
frameWidth = self.style().pixelMetric(QtGui.QStyle.PM_DefaultFrameWidth)
print topRight, bottomRight, frameWidth
# get the rect from QStyle instead of hardcode numbers here
arrowTopLeft = QtCore.QPoint(topRight.x()-20, topRight.y())
arrowRect = QtCore.QRect(arrowTopLeft, bottomRight)
if arrowRect.contains(event.pos()):
print 'clicked near arrow'
# event.accept()
QtGui.QPushButton.mousePressEvent(self, event)
else:
print 'clicked outside'
# call the slot connected, without popup the menu
# the following code now does not make
# the button pressed
self.clicked.emit(True)
event.accept()
class Main(QtGui.QDialog):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
layout = QtGui.QVBoxLayout()
pushbutton = MyButton('Popup Button')
layout.addWidget(pushbutton)
menu = QtGui.QMenu()
menu.addAction('This is Action 1', self.Action1)
menu.addAction('This is Action 2', self.Action2)
pushbutton.setMenu(menu)
self.setLayout(layout)
pushbutton.clicked.connect(self.button_press)
def button_press(self):
print 'You pressed button'
def Action1(self):
print 'You selected Action 1'
def Action2(self):
print 'You selected Action 2'
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Main()
main.show()
app.exec_()
edit:
it seems this will stop the menu from poping up if clicked on the left side of the button
else:
print 'clicked outside'
self.blockSignals(True)
QtGui.QPushButton.mousePressEvent(self, event)
self.blockSignals(False)
Have you thought on using a QComboBox?
Or maybe two buttons next to each other one for appearance only, and the other that calls your context?
Would work to use mask on your button through pixmap.
You also could make some use of setStyleSheet("") can make some use of these attributes.
Here is a little example:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QScrollArea
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class WPopUpButton(QWidget):
"""WPopUpButton is a personalized QPushButton."""
w_container = None
v_layout_container = None
v_scroll_area = None
v_layout_preview = None
def __init__(self):
"""Init UI."""
super(WPopUpButton, self).__init__()
self.init_ui()
def init_ui(self):
"""Init all ui object requirements."""
self.button_that_do_nothing = QPushButton("Popup Button")
self.button_that_do_nothing.setStyleSheet("""
border: 0px;
background: gray;
""")
self.button_that_do_something = QPushButton("->")
#you can also set icon, to make it look better :D
self.button_that_do_something.setStyleSheet("""
border: 0px;
background: gray;
""")
self.layout = QHBoxLayout()
self.layout.setSpacing(0)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.button_that_do_nothing)
self.layout.addWidget(self.button_that_do_something)
self.setLayout(self.layout)
self.create_connections()
def create_connections(self):
self.button_that_do_something.pressed.connect(self.btn_smtg_pressed)
self.button_that_do_something.released.connect(self.btn_smtg_released)
def btn_smtg_pressed(self):
self.button_that_do_something.setStyleSheet("""
border: 0px;
background: blue;
""")
def btn_smtg_released(self):
self.button_that_do_something.setStyleSheet("""
border: 0px;
background: gray;
""")
# HERE YOU DO WHAT YOU NEED
# FOR EXAMPLE CALL YOUR CONTEXT WHATEVER :D
def run():
app = QApplication(sys.argv)
GUI = WPopUpButton()
GUI.show()
sys.exit(app.exec_())
run()
By the way I'm using Pyqt5, you just gotta change your imports ")
Here's another option that may partially answer your question.
Instead of using the default menu, you can combine CustomContextMenu and custom arrow created by either QLabel and/or .png images.
setContentsMargins in the code will allow a much more flexible layout.
sample image
import os
from PyQt5.QtWidgets import (
QDialog,
QPushButton,
QApplication,
QVBoxLayout,
QMenu,
QStyle,
QHBoxLayout,
QLabel,
)
from PyQt5.QtCore import (
QEvent,
QPoint,
QRect,
Qt,
QSize,
)
from PyQt5.QtGui import (
QIcon,
QMouseEvent,
)
import sys
import functools
import copy
class MyButton(QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.clicked_near_arrow = None
# set icon by letter
self.label_icon = QLabel(" ▼ ")
self.label_icon.setAttribute(Qt.WA_TranslucentBackground)
self.label_icon.setAttribute(Qt.WA_TransparentForMouseEvents)
icon_size = QSize(19, 19)
# set icon by picture
self.pixmap_default = QIcon("default_button.png").pixmap(icon_size) # prepare images if necessary
self.pixmap_presssed = QIcon("pressed_button.png").pixmap(icon_size) # prepare images if necessary
self.pic_icon = QLabel()
self.pic_icon.setAttribute(Qt.WA_TranslucentBackground)
self.pic_icon.setAttribute(Qt.WA_TransparentForMouseEvents)
self.pic_icon.setPixmap(self.pixmap_default)
# layout
lay = QHBoxLayout(self)
lay.setContentsMargins(0, 0, 6, 3)
lay.setSpacing(0)
lay.addStretch(1)
lay.addWidget(self.pic_icon)
lay.addWidget(self.label_icon)
def set_icon(self, pressed):
if pressed:
self.label_icon.setStyleSheet("QLabel{color:white}")
self.pic_icon.setPixmap(self.pixmap_presssed)
else:
self.label_icon.setStyleSheet("QLabel{color:black}")
self.pic_icon.setPixmap(self.pixmap_default)
def mousePressEvent(self, event):
if event.type() == QEvent.MouseButtonPress:
self.set_icon(pressed=True)
# figure out press location
topRight = self.rect().topRight()
bottomRight = self.rect().bottomRight()
# get the rect from QStyle instead of hardcode numbers here
arrowTopLeft = QPoint(topRight.x()-19, topRight.y())
arrowRect = QRect(arrowTopLeft, bottomRight)
if arrowRect.contains(event.pos()):
self.clicked_near_arrow = True
self.blockSignals(True)
QPushButton.mousePressEvent(self, event)
self.blockSignals(False)
print('clicked near arrow')
self.open_context_menu()
else:
self.clicked_near_arrow = False
QPushButton.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
if self.rect().contains(event.pos()):
self.set_icon(pressed=True)
else:
self.set_icon(pressed=False)
QPushButton.mouseMoveEvent(self, event)
def mouseReleaseEvent(self, event):
self.set_icon(pressed=False)
if self.clicked_near_arrow:
self.blockSignals(True)
QPushButton.mouseReleaseEvent(self, event)
self.blockSignals(False)
else:
QPushButton.mouseReleaseEvent(self, event)
def setMenu(self, menu):
self.menu = menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.open_context_menu)
# ContextMenueのlauncher
def open_context_menu(self, point=None):
point = QPoint(7, 23)
self.menu.exec_(self.mapToGlobal(point))
event = QMouseEvent(QEvent.MouseButtonRelease, QPoint(10, 10), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
self.mouseReleaseEvent(event)
class Main(QDialog):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
menu = QMenu()
menu.addAction('This is Action 1', self.Action1)
menu.addAction('This is Action 2', self.Action2)
pushbutton = MyButton('Popup Button')
pushbutton.setMenu(menu)
layout = QVBoxLayout()
layout.addWidget(pushbutton)
self.setLayout(layout)
# event connect
pushbutton.setAutoDefault(False)
pushbutton.clicked.connect(self.button_press)
def button_press(self):
print('You pressed button')
def Action1(self):
print('You selected Action 1')
def Action2(self):
print('You selected Action 2')
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Main()
main.show()
app.exec_()