[PyQt] Shaped menus... better than pie menus?

Martin Blais blais at furius.ca
Fri Oct 5 02:05:55 BST 2007


http://furius.ca/blog/doc/ae98bfb7-ef3c-49aa-ac84-1b363900c62b/html
I thought this might be of interest to some of you guys.
Screenshots in attachment.


============================
   Better than Pie Menus?
============================
:Id: ae98bfb7-ef3c-49aa-ac84-1b363900c62b
:Tags: Tech, Qt
:Disclosure: public
:Location: Beverly Hills, Los Angeles, California, U.S.A.
:Date: 2007-10-04


Wow. Today I was looking at options for implementing pie menus in a
PyQt application, and came up with a really nice general class that
does shaped menus driven by a UI file instead.

At the time of writing, my options for using pie menus in Qt are as
follows:

- Purchase Qt's QPieMenu widget, which is licensed in the Qt Solutions
  (an add-on to Qt's normal licensing), a C++ class that then would
  need to be wrapped with sip for usage from PyQt;

- Use the open source widget written by Peter Strath, as part of his
  NeoUI project. Unfortunately, this widget is written for qt3 and my
  application is written in qt4.

- Write your own PyQt4 pie menu class from scratch.

I'm not sure that what I wanted is a pie menu. The task that is
calling for this is rather experimental, and I work for a very
eccentric genius who is likely on our next status report and demo to
ask me to add checkboxes and combo lists within the menu... what I
would really need, is something like a pie menu, a menu that displays
in a shaped window, and that I can customize easily to comply with the
changing requirements of my program. I would do well to use the Qt
Designer for this.

I set out to write a small prototype of a shaped widget whose children
are loaded from a UI description file. It turns out that there is an
easy and reliable way to implement this kind of widget. Here is what I
came up with::

  """
  Custom menu classes.
  """

  from PyQt4 import uic
  from PyQt4.QtCore import QPoint, QEventLoop, Qt, SIGNAL, QObject
  from PyQt4.QtGui import QWidget, QCursor, QApplication, QPainter, QColor


  class QShapedMenu(QWidget):
      """
      A menu class that reads its child widgets from a .ui file and displays as a
      shaped window of those widgets. Create it with a .ui filename, and use it as
      you would a QMenu.
      """
      def __init__(self, uifilename, parent=None):
          QWidget.__init__(self, parent, Qt.Popup)

          # Load and create the UI for the menu.
          BaseGen, BaseClass = uic.loadUiType(uifilename)
          self.ui = BaseGen()
          self.ui.setupUi(self)

          # A nested event loop.
          self.loop = None

          # True if the window was canceled.
          self.iscanceled = True

          # Default result value.
          self.result = None

      def resizeEvent(self, e):
          "Set the mask the widget to the region defined by the children."
          self.setMask(self.childrenRegion())

      def closeEvent(self, e):
          "Because this is a menu, when it gets closed, we exit the subloop."
          if self.loop:
              self.loop.exit()
          QWidget.closeEvent(self, e)

      def mousePressEvent(self, e):
          """If the user clicks outside the mask, we close the menu, just like
          when he clicks outside the menu."""
          if not self.mask().contains(e.pos()):
              self.cancel()
          QWidget.mousePressEvent

      def popup(self, point=None):
          """Show the menu centered around the given point. If no point is
          specified, show the menu around the cursor."""
          if point is None:
              point = QCursor.pos()
          s = self.size()/2
          upleft = QPoint(point.x()-s.width(), point.y()-s.height())
          self.move(upleft)
          self.show()

      def exec_(self):
          "Run an event loop for the menu and return the results."
          self.loop = QEventLoop()
          self.loop.exec_()
          return self.getResult()

      def accept(self, result=None):
          "Quit the menu. Call this when an action should close it."
          self.iscanceled = False
          self.result = result
          self.close()

      def cancel(self):
          "Cancel the menu."
          self.iscanceled = True
          self.close()

      def getResult(self):
          "Override this method to provide a result for the exec_() function."
          return self.result



  if __name__ == "__main__": # Test

      class TestWidget(QWidget):

          def contextMenuEvent(self, e):
              menu = QShapedMenu('testmenu.ui', self)
              for name in 'left', 'right', 'up', 'down':
                  QObject.connect(getattr(menu.ui, name), SIGNAL("clicked()"),
                                  lambda name=name: menu.accept(name))
              menu.popup()
              print 'Enter menu...'
              result = menu.exec_()
              print '...exit menu: %s' % result

          def paintEvent(self, e):
              "We want to draw the background with a different color."
              painter = QPainter(self)
              painter.fillRect(self.rect(), QColor(100, 123, 123))
              painter.end()

      # Create a test application and run it.
      app = QApplication([])
      test = TestWidget()
      test.show()
      app.exec_()


Just run the code to see the resulting menu. It works great, I can add
all kinds of widgets in there and it still works the same. You use it
like this::

  menu = QShapedMenu('testmenu.ui', self)
  ... connect the widgets from menu.ui.XXX to signals ...
  ... the callbacks shoudl call either menu.cancel() or menu.accept()
  menu.popup()
  result = menu.exec_()

You can also create your own custom menu class and bind callbacks
locally::

  class MyMenu(QShapedMenu):

      def __init__(self, parent=None):
          QShapedMenu.__init__(self, 'mymenu.ui', parent)
          QObject.connect(self.ui.myButton, SIGNAL("clicked()"), self.buttonPressed)

      def buttonPressed(self):
          self.accept()

You can probably implement multiple levels of menus by recursing to
other shaped menus in your callbacks.

Disadvantages over real pie menus:

- The shaped menu is not really fit for creating menus with an amount
  of widgets that are not predefined (but you could easily modify it
  to do so--it only depends on having children added to it and laid
  out appropriately).

- This class does not support the more advanced features of pie menus,
  like delayed display and gesture recognition.




More information about the PyQt mailing list