4

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?

  1. [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.

  2. 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?

img

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)
Shuman
  • 3,914
  • 8
  • 42
  • 65

2 Answers2

1
  1. Have you thought on using a QComboBox?
  2. Or maybe two buttons next to each other one for appearance only, and the other that calls your context?
  3. Would work to use mask on your button through pixmap.
  4. 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 ")

yurisnm
  • 1,630
  • 13
  • 29
0

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_()
matcha
  • 21
  • 3