When you click on QScrollBar's "page control area" ('c' on the image), it will scroll one page. What I want is to make it scroll to the full, just like when you choose "Scroll here" context menu item.
That's quite an interesting question. To find out an answer, we need to take a look at QScrollBar source and find out two things:
How to determine which part of the scroll bar has been clicked;
How to trigger "Scroll Here" behavior.
The answer to the first question lies in QScrollBar::mousePressEvent implementation. It turns out that QStyle::hitTestComplexControl does just what we need. What for the second question, just search "Scroll here" and you'll see that QScrollBarPrivate::pixelPosToRangeValue is used to convert event position to slider value. Unfortunately, we don't have access to functions of this private class, so we're forced to reimplement it. Now let's apply gained knowledge and implement new behavior in a subclass:
import sys
from PyQt4 import QtCore, QtGui
class ModifiedScrollBar(QtGui.QScrollBar):
def __init__(self, parent = None):
super(ModifiedScrollBar, self).__init__(parent)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
opt = QtGui.QStyleOptionSlider()
self.initStyleOption(opt)
control = self.style().hitTestComplexControl(QtGui.QStyle.CC_ScrollBar, opt,
event.pos(), self)
if (control == QtGui.QStyle.SC_ScrollBarAddPage or
control == QtGui.QStyle.SC_ScrollBarSubPage):
# scroll here
gr = self.style().subControlRect(QtGui.QStyle.CC_ScrollBar, opt,
QtGui.QStyle.SC_ScrollBarGroove, self)
sr = self.style().subControlRect(QtGui.QStyle.CC_ScrollBar, opt,
QtGui.QStyle.SC_ScrollBarSlider, self)
if self.orientation() == QtCore.Qt.Horizontal:
pos = event.pos().x()
sliderLength = sr.width()
sliderMin = gr.x()
sliderMax = gr.right() - sliderLength + 1
if (self.layoutDirection() == QtCore.Qt.RightToLeft):
opt.upsideDown = not opt.upsideDown
else:
pos = event.pos().y()
sliderLength = sr.height()
sliderMin = gr.y()
sliderMax = gr.bottom() - sliderLength + 1
self.setValue(QtGui.QStyle.sliderValueFromPosition(
self.minimum(), self.maximum(), pos - sliderMin,
sliderMax - sliderMin, opt.upsideDown))
return
return super(ModifiedScrollBar, self).mousePressEvent(event)
def main():
app = QtGui.QApplication(sys.argv)
edit = QtGui.QTextEdit()
#uncomment for testing horizontal scrollbar
#edit.setLineWrapMode(QtGui.QTextEdit.NoWrap)
edit.setPlainText("Lorem ipsum...")
edit.setVerticalScrollBar(ModifiedScrollBar())
edit.setHorizontalScrollBar(ModifiedScrollBar())
edit.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Works for me, python 3.4 pyqt 5.4.
pixelPosToRangeValue is taken from qt source
def wrapEF(ef):
w = QObject()
w.eventFilter = ef
return w
def sbEventFilter(s, e):
q = s
if (e.type() == QEvent.MouseButtonPress and e.button() == Qt.LeftButton
or e.type() == QEvent.MouseButtonDblClick):
#pixelPosToRangeValue(pos)
opt = QStyleOptionSlider()
q.initStyleOption(opt)
gr = q.style().subControlRect(QStyle.CC_ScrollBar, opt,
QStyle.SC_ScrollBarGroove, q)
sr = q.style().subControlRect(QStyle.CC_ScrollBar, opt,
QStyle.SC_ScrollBarSlider, q)
if q.orientation() == Qt.Horizontal:
sliderLength = sr.width()
sliderMin = gr.x()
sliderMax = gr.right() - sliderLength + 1
if q.layoutDirection() == Qt.RightToLeft:
opt.upsideDown = not opt.upsideDown
dt = sr.width()/2
pos = e.pos().x()
else:
sliderLength = sr.height()
sliderMin = gr.y()
sliderMax = gr.bottom() - sliderLength + 1
dt = sr.height()/2
pos = e.pos().y()
r = QStyle.sliderValueFromPosition(q.minimum(), q.maximum(),
pos - sliderMin - dt,
sliderMax - sliderMin, opt.upsideDown)
#pixelPosToRangeValue,
q.setValue(r)
return q.eventFilter(s, e)
self.scrollBarEF = wrapEF(sbEventFilter)
self.hscrollbar.installEventFilter(self.scrollBarEF)
self.vscrollbar.installEventFilter(self.scrollBarEF)
Related
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I am trying to make small program in Python,using PyQt5.The program will as have many button has arranged neatly.When dragging one of the buttons to another button, you can swap their positions while the buttons are still arranged neatly.I tried many ways, but in the end the buttons couldn’t be arranged neatly.What should I do to achieve this effect
Sounds like a job for QEventFilter.
Simple version:
from PyQt5.QtCore import QEvent, QObject
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QPushButton
class Filter(QObject):
def __init__(self, parent = None):
super().__init__(parent)
self._pressed = None
self._drag = False
def setDragMode(self, active):
""" When drag mode activated buttons are not clickable (mouse events are filtered) """
self._drag = active
def setLayout(self, layout):
self._layout = layout
for i in range(layout.count()):
item = layout.itemAt(i)
if item.widget():
item.widget().installEventFilter(self)
def eventFilter(self, obj, event):
if not self._drag:
return False
if event.type() == QEvent.MouseButtonPress:
if isinstance(obj, QPushButton):
self._pressed = obj
return True
elif event.type() == QEvent.MouseMove:
button = self._pressed
layout = self._layout
if button is None:
return True
pos = obj.mapToParent(event.pos())
index = layout.indexOf(button)
geometry = button.geometry()
if pos.x() < geometry.topLeft().x():
if index - 1 >= 0:
prev = layout.itemAt(index - 1).widget().geometry()
if prev.topRight().x() > pos.x():
layout.insertWidget(index - 1, button)
elif pos.x() > geometry.topRight().x():
if index + 1 < layout.count():
next_ = layout.itemAt(index + 1).widget().geometry()
if next_.topLeft().x() < pos.x():
layout.insertWidget(index + 1, button)
return True
elif event.type() == QEvent.MouseButtonRelease:
self._pressed = None
return True
return False
if __name__ == "__main__":
app = QApplication([])
widget = QWidget()
layout = QHBoxLayout()
buttons = [QPushButton("button {}".format(i)) for i in range(5)]
for button in buttons:
layout.addWidget(button)
widget.setLayout(layout)
widget.show()
filter_ = Filter()
filter_.setLayout(layout)
filter_.setDragMode(True)
app.exec_()
More intuitive and interactive version:
from PyQt5.QtCore import QEvent, QObject
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QPushButton
class Overlay(QWidget):
def __init__(self, parent = None):
super().__init__(parent)
self._widget = None
self._pressPos = None
self._topLeftPos = None
self._movePos = None
self._pixmap = None
def setDragWidget(self, widget, pressPos, topLeftPos):
self._widget = widget
self._pressPos = pressPos
self._movePos = pressPos
self._topLeftPos = topLeftPos
self._pixmap = widget.grab()
def mouseMoved(self, pos):
self._movePos = pos
self.update()
def mouseReleased(self):
self._pixmap = None
self.update()
def paintEvent(self, event):
if self._pixmap is None:
return
painter = QPainter(self)
painter.drawPixmap(self._topLeftPos + self._movePos - self._pressPos, self._pixmap)
def swap_widgets(layout, index1, index2):
if index1 == index2:
return
widget1 = layout.itemAt(index1).widget()
widget2 = layout.itemAt(index2).widget()
layout.insertWidget(index1, widget2)
layout.insertWidget(index2, widget1)
class Filter(QObject):
def __init__(self, parent = None):
super().__init__(parent)
self._pressed = None
self._drag = False
self._overlay = None
def setDragMode(self, active):
""" When drag mode activated buttons are not clickable (mouse events are filtered) """
self._drag = active
def setLayout(self, layout):
self._layout = layout
for i in range(layout.count()):
item = layout.itemAt(i)
if item.widget():
item.widget().installEventFilter(self)
def eventFilter(self, obj, event):
if not self._drag:
return False
if event.type() == QEvent.MouseButtonPress:
if isinstance(obj, QPushButton):
self._pressed = obj
parent = obj.parent()
overlay = Overlay(parent)
overlay.setGeometry(parent.rect())
pressPos = obj.mapToParent(event.pos())
overlay.setDragWidget(obj, pressPos, obj.geometry().topLeft())
overlay.show()
self._overlay = overlay
return True
elif event.type() == QEvent.MouseMove:
button = self._pressed
layout = self._layout
if button is None:
return True
pos = obj.mapToParent(event.pos())
overlay = self._overlay
if overlay is not None:
overlay.mouseMoved(pos)
return True
elif event.type() == QEvent.MouseButtonRelease:
overlay = self._overlay
button = self._pressed
layout = self._layout
if overlay is not None:
overlay.mouseReleased()
overlay.hide()
pos = obj.mapToParent(event.pos())
index1 = layout.indexOf(button)
for index2 in range(layout.count()):
widget = layout.itemAt(index2).widget()
if widget is None:
continue
if widget.geometry().contains(pos):
swap_widgets(layout, index1, index2)
break
self._pressed = None
return True
return False
if __name__ == "__main__":
app = QApplication([])
widget = QWidget()
layout = QHBoxLayout()
buttons = [QPushButton("button {}".format(i)) for i in range(5)]
for button in buttons:
layout.addWidget(button)
widget.setLayout(layout)
widget.show()
filter_ = Filter()
filter_.setLayout(layout)
filter_.setDragMode(True)
app.exec_()
I can add self.setWindowFlags(Qt.Window | Qt.WindowContextHelpButtonHint | Qt.WindowCloseButtonHint) to the constructor of my QMainWindow which adds the '?' button. Now I want to add contextual help information to virtually all UI components. How do I connect a click on the '?' button and then a click on some ui element (say a qlistview) such that a help message pops up?
Below is part of my program stripped to make it easy. How do I make it such that when the user clicks the '?' and then either a QDoubleSpinBox or the QListView or any of the QPushButtons that a help message displays?
I have searched for this but it seems the vast amount of questions and answers are about how to remove the '?' button or how to add it. None actually deal with how to alter the button's behaviour.
I've also tried using setInputMethodHints() to assign hints to ui components but this method doesn't accept custom strings as I assumed it would.
Btw: I also would like the '?' button to not replace the minimization button. I have yet to figure out how to achieve this either.
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
APPNAME = 'Mesoscale Brain Explorer'
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle(APPNAME)
self.sidebar = Sidebar()
self.setWindowFlags(Qt.Window | Qt.WindowContextHelpButtonHint | Qt.WindowCloseButtonHint)
self.setup_ui()
def setup_ui(self):
self.pl_frame = QFrame()
splitter = QSplitter(self)
self.enable = lambda yes: splitter.setEnabled(yes)
splitter.setHandleWidth(3)
splitter.setStyleSheet('QSplitter::handle {background: #cccccc;}')
splitter.addWidget(self.sidebar)
splitter.addWidget(self.pl_frame)
self.setCentralWidget(splitter)
self.menu = self.menuBar()
m = self.menu.addMenu('&File')
a = QAction('&New project', self)
a.setShortcut('Ctrl+N')
a.setStatusTip('Create new project')
m.addAction(a)
a = QAction('&Open project', self)
a.setShortcut('Ctrl+O')
a.setStatusTip('Open project')
m.addAction(a)
a = QAction("&Quit", self)
a.setShortcut("Ctrl+Q")
a.setStatusTip('Leave The App')
a.setIcon(QIcon('pics/quit.png'))
m.addAction(a)
about_action = QAction('&About ' + APPNAME, self)
about_action.setStatusTip('About ' + APPNAME)
about_action.setShortcut('F1')
m = self.menu.addMenu('&Project')
m.setEnabled(False)
a = QAction("&Close", self)
a.setStatusTip('Close project')
m.addAction(a)
self.project_menu = m
help_menu = self.menu.addMenu('&Help')
help_menu.addAction(about_action)
class Sidebar(QWidget):
open_pipeconf_requested = pyqtSignal()
open_datadialog_requested = pyqtSignal()
automate_pipeline_requested = pyqtSignal()
x_origin_changed = pyqtSignal(float)
y_origin_changed = pyqtSignal(float)
units_per_pixel_changed = pyqtSignal(float)
def __init__(self, parent=None):
super(Sidebar, self).__init__(parent)
self.x_origin = QDoubleSpinBox()
self.y_origin = QDoubleSpinBox()
self.units_per_pixel = QDoubleSpinBox()
self.setup_ui()
def setup_ui(self):
self.setContentsMargins(4, 6, 5, 0)
vbox = QVBoxLayout()
hbox = QHBoxLayout()
hbox.addWidget(QLabel('Origin:'))
hbox.addWidget(QLabel('Units per pixel:'))
vbox.addLayout(hbox)
hbox2 = QHBoxLayout()
hhbox = QHBoxLayout()
hhbox2 = QHBoxLayout()
hhbox.addWidget(QLabel("X:"))
hhbox.addWidget(self.x_origin)
hhbox.addWidget(QLabel("Y:"))
hhbox.addWidget(self.y_origin)
hhbox2.addWidget(self.units_per_pixel)
hbox2.addLayout(hhbox)
hbox2.addLayout(hhbox2)
vbox.addLayout(hbox2)
self.units_per_pixel.setDecimals(5)
self.units_per_pixel.setMaximum(100000)
self.x_origin.setMaximum(100000)
self.y_origin.setMaximum(100000)
self.pl_list = QListView()
self.pl_list.setIconSize(QSize(18, 18))
self.pl_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.pl_list.setEditTriggers(QAbstractItemView.NoEditTriggers)
vbox.addWidget(QLabel('Pipeline:'))
vbox.addWidget(self.pl_list)
pb = QPushButton('&Automation')
pb.clicked.connect(self.automate_pipeline_requested)
vbox.addWidget(pb)
vbox.addSpacerItem(QSpacerItem(0, 1, QSizePolicy.Minimum, QSizePolicy.Expanding))
pb = QPushButton('&Configure Pipeline')
pb.clicked.connect(self.open_pipeconf_requested)
vbox.addWidget(pb)
pb = QPushButton('&Manage Data')
pb.clicked.connect(self.open_datadialog_requested)
vbox.addWidget(pb)
vbox.setStretch(0, 0)
vbox.setStretch(1, 0)
vbox.setStretch(2, 0)
self.setLayout(vbox)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setApplicationName(APPNAME)
app.setOrganizationName('University of British Columbia')
w = MainWindow()
w.resize(1060, 660)
w.show()
app.exec_()
app.deleteLater()
del w
sys.exit()
I am trying to save a text file from the first page of a flatNotebook, write them to an EXTERNALLY defined sqLite database and write the values to a grid on the second page of the flatNotebook. The values are saved to the text file and written to the database successfully but I cannot get the values to populate the grid at the same time. They values show up in the grid after I close the program and restart it. I am having a hard time understanding how to call the function onAddCue(). I have tried so many things that I'm just confusing myself at this point. Please help me understand what I am doing wrong. Here is my entire code:
cue =[4,'NodeA',11,22,33,44,55,66,77,88,99]
class InitialInputs(scrolled.ScrolledPanel):
global cue
def __init__(self, parent, db):
scrolled.ScrolledPanel.__init__(self, parent, -1)
self.db = db
self.cur = self.db.con.cursor()
self.saveBtn = wx.Button(self, -1, "Save Current Values")
self.Bind(wx.EVT_BUTTON, self.onSave, self.saveBtn)
self.dirname = ""
def onSave(self, event):
global cue
dlg = wx.FileDialog(self, "Choose a file", self.dirname, "", "*.txt", wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
if dlg.ShowModal() == wx.ID_OK:
itcontains = cue
self.filename=dlg.GetFilename()
self.dirname=dlg.GetDirectory()
filehandle=open(os.path.join(self.dirname, self.filename),'w')
filehandle.write(str(itcontains))
filehandle.close()
dlg.Destroy()
row = cue[0] - 1
InsertCell ="UPDATE CUES SET 'Send'=?,'RED'=?,'GREEN'=?,'BLUE'=?,'RGB_Alpha'=?,'HUE'=?,'SAT'=?,'BRightness'=?,'HSB_Alpha'=?,'Fade'=? WHERE DTIndex=%i" %row
self.cur.execute(InsertCell, cue[1:])
self.db.con.commit()
GridPanel().grid.onAddCue() #This is the part that's not working
class Grid(gridlib.Grid):
global cue
def __init__(self, parent, db):
gridlib.Grid.__init__(self, parent, -1)
self.CreateGrid(20,10)
for row in range(20):
rowNum = row + 1
self.SetRowLabelValue(row, "cue %s" %rowNum)
self.db = db
self.cur = self.db.con.cursor()
meta = self.cur.execute("SELECT * from CUES")
labels = []
for i in meta.description:
labels.append(i[0])
labels = labels[1:]
for i in range(len(labels)):
self.SetColLabelValue(i, labels[i])
all = self.cur.execute("SELECT * from CUES ORDER by DTindex")
for row in all:
row_num = row[0]
cells = row[1:]
for i in range(len(cells)):
if cells[i] != None and cells[i] != "null":
self.SetCellValue(row_num, i, str(cells[i]))
self.Bind(gridlib.EVT_GRID_CELL_CHANGED, self.CellContentsChanged)
def CellContentsChanged(self, event):
x = event.GetCol()
y = event.GetRow()
val = self.GetCellValue(y,x)
if val == "":
val = "null"
ColLabel = self.GetColLabelValue(x)
InsertCell = "UPDATE CUES SET %s = ? WHERE DTindex = %d"%(ColLabel,y)
self.cur.execute(InsertCell, [(val),])
self.db.con.commit()
self.SetCellValue(y, x, val)
class GridPanel(wx.Panel):
def __init__(self, parent, db):
wx.Panel.__init__(self, parent, -1)
self.db = db
self.cur = self.db.con.cursor()
grid = Grid(self, db)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(grid)
self.SetSizer(sizer)
self.Fit()
def onAddCue():
global cue
row = cue[0] - 1
col=0
while col < 10:
for i in cue[1:]:
grid.SetCellValue(row, col, str(i))
col = col+1
class GetDatabase():
def __init__(self, f):
# check db file exists
try:
file = open(f)
file.close()
except IOError:
# database doesn't exist - create file & populate it
self.exists = 0
else:
# database already exists - need integrity check here
self.exists = 1
self.con = sqlite.connect(f)
class Frame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1,"Stage Lighting", size=(800,600))
panel = wx.Panel(self)
notebook = wx.Notebook(panel)
page1 = InitialInputs(notebook, db)
page2 = GridPanel(notebook, db)
notebook.AddPage(page1, "Initial Inputs")
notebook.AddPage(page2, "Grid")
sizer = wx.BoxSizer()
sizer.Add(notebook, 1, wx.ALL|wx.EXPAND, 5)
panel.SetSizer(sizer)
self.Layout()
if __name__ == "__main__":
db = GetDatabase("data.db")
app = wx.App()
logging.basicConfig(level=logging.DEBUG)
Frame().Show()
app.MainLoop()
Apparently, the question is about python syntax errors!
onAddCue is a method belonging to the class GridPanel. The class is instantiated in the variable page2.
So, you need to call the method something like this:
page2.OnAddCue()
I'm trying to implement a pinterest-like UI and I've been experimenting with the FlowLayout example for PySide.
The problem is that I'm trying to use a QScrollArea and it won't let me scroll down enough. In other words, I can't get to the bottom of the widget I'm trying to scroll.
You can get the hole file here. If you download it and run it, the problem will become pretty obvious.
My code was taken from here, with these modifications:
Added this class: (it just let's me generate colored and numerated rectangles)
class ColoredRectangle(QLabel):
count = 0
def __init__(self, height, color):
super(ColoredRectangle, self).__init__()
palette = QPalette()
palette.setColor(QPalette.Background, color)
self.setAutoFillBackground(True)
self.setPalette(palette)
self.setFixedSize(200, height)
self.setText(str(ColoredRectangle.count))
ColoredRectangle.count += 1
Now I'm adding rectangles instead of buttons...
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
flowLayout = FlowLayout()
flowLayout.addWidget(ColoredRectangle(450, Qt.white))
flowLayout.addWidget(ColoredRectangle(200, Qt.green))self.setLayout(flowLayout)
self.setLayout(flowLayout)
self.setWindowTitle("Flow Layout")
Changed the doLayout method like this:
def doLayout(self, rect):
if not any(self.itemList):
return
firstItem = self.itemList[0]
x = rect.x()
y = rect.y()
wid = firstItem.widget()
spaceX = self.spacing() + wid.style().layoutSpacing(QtGui.QSizePolicy.PushButton,
QtGui.QSizePolicy.PushButton, QtCore.Qt.Horizontal)
spaceY = self.spacing() + wid.style().layoutSpacing(QtGui.QSizePolicy.PushButton,
QtGui.QSizePolicy.PushButton, QtCore.Qt.Vertical)
itemWidth = firstItem.sizeHint().width()
# calculate column count
columnCount = (rect.width() + spaceX) / (itemWidth + spaceX)
availablePositions = [None] * columnCount
for i in range(columnCount):
availablePositions[i] = QPoint(x + i * (itemWidth + spaceX), y)
for item in self.itemList:
currentPosition = availablePositions[0]
currentPositionColumn = 0
positionColumn = 0
for position in availablePositions:
if min(currentPosition.y(), position.y()) != currentPosition.y():
currentPositionColumn = positionColumn
currentPosition = position
positionColumn += 1
# update available position in column
itemSize = item.sizeHint()
availablePositions[currentPositionColumn] = QPoint(currentPosition.x(),
currentPosition.y() + itemSize.height() + spaceY)
# place item
item.setGeometry(QtCore.QRect(currentPosition, itemSize))
And finally, I added the QScrollArea:
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
mainWin = Window()
scrollArea = QScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setWidget(mainWin)
scrollArea.show()
sys.exit(app.exec_())
Thanks in advance!
Well, found the answer on my own...
Apparently the layout has to define the method heightForWidth, which, as far as I understand, returns the height of the layout which results after a width resize. So... I had to store the new height at the end of doLayout so I'm able to return it when asked for.
This is what I did after doLayout:
self.heightForWidthValue = max(map(lambda position : position.y(), availablePositions))
Also, I implemented these functions in FlowLayout:
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
return self.heightForWidthValue
And set 0 as the default value (initialized in FlowLayout->__init__) for heightForWidthValue.
Now it works like a charm :)
Here's the proper code, if anyone is interested.
Is there a way in Qt to add a layout or widgets to a QMenu to create a custom menu?
The example below (left) is what I have, and I'd like to aim for something similar to the mock-up on the right, by adding non-menu widgets. If it can't be done by QMenu, are there guides to produce similar results (perhaps by having a more standard widget act as a context menu) anywhere?
Sure there is! In Qt, if there is a a will there is a way.
You will probably need to make your own class that uses QMenu and uses a member QListWidget.
You will then need to generate the layout and overload all the correct QLayout functions for size recalculation.
And then you'll need to use this layout (think QHBoxLayout) to display both a QMenu and a QListWidget side by side.
That should be enough to point you in the right direction.
EDIT:
As a commenter pointed out, you can't inherit two QObject things so I updated the
answer accordingly.
To customize menu items you can use QWidgetAction class. But you want to customize menu to look like popup widget. So you may subclass QMenu and try to improve layout of menu for your needs (QMenu is QWidget). You questoin is not clear.
I wrote a script, you can try it.
but I am not subclass QMenu.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PySide.QtGui import *
from PySide.QtCore import *
class MenuItem(QWidget):
"""docstring for MenuItem"""
def __init__(self, text='test', icon=None, parent=None):
super(MenuItem, self).__init__(parent)
hbox = QHBoxLayout(self)
# hbox.setContentsMargins(0, 0, 0, 0)
label = QLabel(text)
btn = QPushButton()
if icon:
btn.setIcon(icon)
hbox.addWidget(label)
hbox.addStretch()
hbox.addWidget(btn)
self.setMinimumWidth(parent.width())
class MyMenu(QWidget):
"""docstring for MyMenu"""
def __init__(self, parent=None):
super(MyMenu, self).__init__(parent)
self.main_width = 200
self.main_height = 150
self.close_menu = False
self.parent = parent
self.setGeometry(0, 0, 200, 150)
self.initUI()
self.setWindowFlags(Qt.Popup)
# self.setWindowModality(Qt.WindowModal)
def initUI(self):
main_frame = QWidget(self)
main_v_layout = QVBoxLayout(main_frame)
main_v_layout.setContentsMargins(0, 0, 0, 0)
item_1 = MenuItem('item 1', parent=self)
item_2 = MenuItem('item 2', parent=self)
item_3 = MenuItem('item 3', parent=self)
main_v_layout.addWidget(item_1)
main_v_layout.addWidget(item_2)
main_v_layout.addWidget(item_3)
def animationShow(self):
self.close_menu = False
self.start_close_menu = True
self.show()
# PyQt4.QtCore.QRect(0, 0, 400, 23)
rect = self.parent.rect()
# PyQt4.QtCore.QPoint(199, 11)
center_pos = rect.center()
# PyQt4.QtCore.QPoint(654, 465)
global_center_pos = self.parent.mapToGlobal(center_pos)
height = rect.height()
show_pos = QPoint(
global_center_pos.x() - (self.width() / 2),
global_center_pos.y() + height)
# print show_pos
self.move(show_pos)
self.inAnimation(show_pos)
def inAnimation(self, show_pos=None):
start_height = QSize(self.main_width, 0)
end_height = QSize(self.main_width, self.main_height)
size_anim = QPropertyAnimation(self, 'size')
size_anim.setStartValue(start_height)
size_anim.setEndValue(end_height)
size_anim.setDuration(160)
size_anim.setEasingCurve(QEasingCurve.OutQuad)
opacity_anim = QPropertyAnimation(self, 'windowOpacity')
opacity_anim.setStartValue(0.0)
opacity_anim.setEndValue(1.0)
opacity_anim.setDuration(260)
opacity_anim.setEasingCurve(QEasingCurve.OutQuad)
self.in_anim_group = QParallelAnimationGroup()
self.in_anim_group.addAnimation(size_anim)
self.in_anim_group.addAnimation(opacity_anim)
self.in_anim_group.start()
def outAnimation(self):
try:
end_size = QSize(self.size().width(), 0)
pos_anim = QPropertyAnimation(self, 'size')
pos_anim.setEndValue(end_size)
pos_anim.setDuration(200)
pos_anim.setEasingCurve(QEasingCurve.InQuad)
opacity_anim = QPropertyAnimation(self, 'windowOpacity')
opacity_anim.setStartValue(1.0)
opacity_anim.setEndValue(0.0)
opacity_anim.setDuration(200)
opacity_anim.setEasingCurve(QEasingCurve.InQuad)
self.out_anim_group = QParallelAnimationGroup()
self.out_anim_group.addAnimation(pos_anim)
self.out_anim_group.addAnimation(opacity_anim)
self.out_anim_group.finished.connect(self.closeMenu)
self.out_anim_group.start()
except RuntimeError as e:
pass
except Exception as e:
print e
def closeMenu(self):
self.close_menu = True
self.setVisible(False)
def closeEvent(self, event):
# super(MyMenu, self).closeEvent(event)
if self.start_close_menu:
self.outAnimation()
self.start_close_menu = False
def hideEvent(self, event):
# print 'hideEvent', event
super(MyMenu, self).hideEvent(event)
def setVisible(self, visible):
if self.close_menu:
visible = False
elif not visible:
visible = True
super(MyMenu, self).setVisible(visible)
class Win(QWidget):
"""docstring for Win"""
def __init__(self):
super(Win, self).__init__()
vbox = QVBoxLayout(self)
btn = QPushButton('call menu')
vbox.addWidget(btn)
self.menu = MyMenu(btn)
btn.clicked.connect(self.menu.animationShow)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Win()
win.show()
sys.exit(app.exec_())