[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