PyQt5 tooltip with clickable hyperlink? - python

Can you suggest me a way to make hyperlink in PyQt5 tooltip clickable? Tried like this:
from PyQt5 import QtWidgets
app = QtWidgets.QApplication([])
w = QtWidgets.QMainWindow()
QtWidgets.QLabel(parent = w, text = 'Hover mouse here', toolTip = 'Unclickable link')
w.show()
app.exec_()
Link is visible, but not clickable, unfortunatelly.

This is not an easy task to achieve.
One of the most important aspects is that users are accustomed to the conventional behavior of tool tips: if the mouse cursor hovers them (or they are clicked), they would probably disappear; this is to avoid any possibility that some important part of the widget they refer to becomes hidden (imagine a table that shows a big tooltip for a cell and hides the values of other cells that are near the first one).
Qt follows the same concepts; so, not only you can't interactively click on a tooltip, but it's usually almost impossible to hover a tooltip at all.
The only solution is to create your own tooltip.
In the following (rather complex) example, I'm going to show how to achieve that.
Note that this implementation is not perfect: I could test it under Linux only, but, most importantly, it's not application-wide (it could theoretically be possible, though).
The basic concept is to install an event filter on all widgets that could potentially have a clickable url, intercept every QEvent that is of QEvent.ToolTip type, and create a widget that behaves like it afterwards.
I tried to implement it as much as similar to the standard QToolTip object (which is only accessible almost by static methods). The only difference here is that the static method returns the instance, which allows to connect to the linkActivated signal.
class ClickableTooltip(QtWidgets.QLabel):
__instance = None
refWidget = None
refPos = None
menuShowing = False
def __init__(self):
super().__init__(flags=QtCore.Qt.ToolTip)
margin = self.style().pixelMetric(
QtWidgets.QStyle.PM_ToolTipLabelFrameWidth, None, self)
self.setMargin(margin + 1)
self.setForegroundRole(QtGui.QPalette.ToolTipText)
self.setWordWrap(True)
self.mouseTimer = QtCore.QTimer(interval=250, timeout=self.checkCursor)
self.hideTimer = QtCore.QTimer(singleShot=True, timeout=self.hide)
def checkCursor(self):
# ignore if the link context menu is visible
for menu in self.findChildren(
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly):
if menu.isVisible():
return
# an arbitrary check for mouse position; since we have to be able to move
# inside the tooltip margins (standard QToolTip hides itself on hover),
# let's add some margins just for safety
region = QtGui.QRegion(self.geometry().adjusted(-10, -10, 10, 10))
if self.refWidget:
rect = self.refWidget.rect()
rect.moveTopLeft(self.refWidget.mapToGlobal(QtCore.QPoint()))
region |= QtGui.QRegion(rect)
else:
# add a circular region for the mouse cursor possible range
rect = QtCore.QRect(0, 0, 16, 16)
rect.moveCenter(self.refPos)
region |= QtGui.QRegion(rect, QtGui.QRegion.Ellipse)
if QtGui.QCursor.pos() not in region:
self.hide()
def show(self):
super().show()
QtWidgets.QApplication.instance().installEventFilter(self)
def event(self, event):
# just for safety...
if event.type() == QtCore.QEvent.WindowDeactivate:
self.hide()
return super().event(event)
def eventFilter(self, source, event):
# if we detect a mouse button or key press that's not originated from the
# label, assume that the tooltip should be closed; note that widgets that
# have been just mapped ("shown") might return events for their QWindow
# instead of the actual QWidget
if source not in (self, self.windowHandle()) and event.type() in (
QtCore.QEvent.MouseButtonPress, QtCore.QEvent.KeyPress):
self.hide()
return super().eventFilter(source, event)
def move(self, pos):
# ensure that the style has "polished" the widget (font, palette, etc.)
self.ensurePolished()
# ensure that the tooltip is shown within the available screen area
geo = QtCore.QRect(pos, self.sizeHint())
try:
screen = QtWidgets.QApplication.screenAt(pos)
except:
# support for Qt < 5.10
for screen in QtWidgets.QApplication.screens():
if pos in screen.geometry():
break
else:
screen = None
if not screen:
screen = QtWidgets.QApplication.primaryScreen()
screenGeo = screen.availableGeometry()
# screen geometry correction should always consider the top-left corners
# *last* so that at least their beginning text is always visible (that's
# why I used pairs of "if" instead of "if/else"); also note that this
# doesn't take into account right-to-left languages, but that can be
# accounted for by checking QGuiApplication.layoutDirection()
if geo.bottom() > screenGeo.bottom():
geo.moveBottom(screenGeo.bottom())
if geo.top() < screenGeo.top():
geo.moveTop(screenGeo.top())
if geo.right() > screenGeo.right():
geo.moveRight(screenGeo.right())
if geo.left() < screenGeo.left():
geo.moveLeft(screenGeo.left())
super().move(geo.topLeft())
def contextMenuEvent(self, event):
# check the children QMenu objects before showing the menu (which could
# potentially hide the label)
knownChildMenus = set(self.findChildren(
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly))
self.menuShowing = True
super().contextMenuEvent(event)
newMenus = set(self.findChildren(
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly))
if knownChildMenus == newMenus:
# no new context menu? hide!
self.hide()
else:
# hide ourselves as soon as the (new) menus close
for m in knownChildMenus ^ newMenus:
m.aboutToHide.connect(self.hide)
m.aboutToHide.connect(lambda m=m: m.aboutToHide.disconnect())
self.menuShowing = False
def mouseReleaseEvent(self, event):
# click events on link are delivered on button release!
super().mouseReleaseEvent(event)
self.hide()
def hide(self):
if not self.menuShowing:
super().hide()
def hideEvent(self, event):
super().hideEvent(event)
QtWidgets.QApplication.instance().removeEventFilter(self)
self.refWidget.window().removeEventFilter(self)
self.refWidget = self.refPos = None
self.mouseTimer.stop()
self.hideTimer.stop()
def resizeEvent(self, event):
super().resizeEvent(event)
# on some systems the tooltip is not a rectangle, let's "mask" the label
# according to the system defaults
opt = QtWidgets.QStyleOption()
opt.initFrom(self)
mask = QtWidgets.QStyleHintReturnMask()
if self.style().styleHint(
QtWidgets.QStyle.SH_ToolTip_Mask, opt, self, mask):
self.setMask(mask.region)
def paintEvent(self, event):
# we cannot directly draw the label, since a tooltip could have an inner
# border, so let's draw the "background" before that
qp = QtGui.QPainter(self)
opt = QtWidgets.QStyleOption()
opt.initFrom(self)
style = self.style()
style.drawPrimitive(style.PE_PanelTipLabel, opt, qp, self)
# now we paint the label contents
super().paintEvent(event)
#staticmethod
def showText(pos, text:str, parent=None, rect=None, delay=0):
# this is a method similar to QToolTip.showText;
# it reuses an existent instance, but also returns the tooltip so that
# its linkActivated signal can be connected
if ClickableTooltip.__instance is None:
if not text:
return
ClickableTooltip.__instance = ClickableTooltip()
toolTip = ClickableTooltip.__instance
toolTip.mouseTimer.stop()
toolTip.hideTimer.stop()
# disconnect all previously connected signals, if any
try:
toolTip.linkActivated.disconnect()
except:
pass
if not text:
toolTip.hide()
return
toolTip.setText(text)
if parent:
toolTip.refRect = rect
else:
delay = 0
pos += QtCore.QPoint(16, 16)
# adjust the tooltip position if necessary (based on arbitrary margins)
if not toolTip.isVisible() or parent != toolTip.refWidget or (
not parent and toolTip.refPos and
(toolTip.refPos - pos).manhattanLength() > 10):
toolTip.move(pos)
# we assume that, if no parent argument is given, the current activeWindow
# is what we should use as a reference for mouse detection
toolTip.refWidget = parent or QtWidgets.QApplication.activeWindow()
toolTip.refPos = pos
toolTip.show()
toolTip.mouseTimer.start()
if delay:
toolTip.hideTimer.start(delay)
return toolTip
class ToolTipTest(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
count = 1
tip = 'This is link {c}'
for row in range(4):
for col in range(4):
button = QtWidgets.QPushButton('Hello {}'.format(count))
layout.addWidget(button, row, col)
button.setToolTip(tip.format(c=count))
button.installEventFilter(self)
count += 1
def toolTipLinkClicked(self, url):
print(url)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ToolTip and source.toolTip():
toolTip = ClickableTooltip.showText(
QtGui.QCursor.pos(), source.toolTip(), source)
toolTip.linkActivated.connect(self.toolTipLinkClicked)
return True
return super().eventFilter(source, event)

Related

wx.ComboCtrl with wx.ListCtrl highlight background and sizing

I am building a wx.ComboCtrl with a wx.ListCtrl attached. The reason for doing this is because I want to set the foreground colour of the choices (the colour shows the user the status of the item). I want these colours to show when the box is dropped down and when a user has made a selection.
The problem I run into is that on Linux (Ubuntu 20.04), after a selection was made, the background colour of the wx.ComboCtrl remains blue (and the foreground colour remains white), even if I move focus to another widget. It doesn't matter which colour I set for the text to be displayed on the ComboCtrl, it remains white text with a blue background. See screenshot.
I can only get it to show me the default (gray) background with my selected foreground colour if I move the focus to another window and then back to my own window.
In Windows this doesn't happen: after selecting an item, the background colour of the ComboCtrl is default (gray), however it does show a little dotted line around the selection. See screenshot.
Here is the modified demo code that I am using to reproduce the issue. The comments in the code are left overs from some of the things I tried.
#!/usr/bin/env python
import wx
import os
#----------------------------------------------------------------------
#----------------------------------------------------------------------
# This class is used to provide an interface between a ComboCtrl and the
# ListCtrl that is used as the popoup for the combo widget.
class ListCtrlComboPopup(wx.ComboPopup):
def __init__(self):
wx.ComboPopup.__init__(self)
self.lc = None
def AddItem(self, txt, _colour):
self.lc.InsertItem(self.lc.GetItemCount(), txt)
_entry = self.lc.GetItem(self.lc.GetItemCount() - 1)
_entry.SetTextColour(_colour)
#_entry.SetItemTextColour(_colour)
self.lc.SetItem(_entry)
def OnMotion(self, evt):
item, flags = self.lc.HitTest(evt.GetPosition())
if item >= 0:
self.lc.Select(item)
self.curitem = item
def OnLeftDown(self, evt):
self.value = self.curitem
self.Dismiss()
# The following methods are those that are overridable from the
# ComboPopup base class. Most of them are not required, but all
# are shown here for demonstration purposes.
# This is called immediately after construction finishes. You can
# use self.GetCombo if needed to get to the ComboCtrl instance.
def Init(self):
self.value = -1
self.curitem = -1
# Create the popup child control. Return true for success.
def Create(self, parent):
self.lc = wx.ListCtrl(parent, style=wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER | wx.LC_REPORT | wx.LC_NO_HEADER)
self.lc.InsertColumn(0, '')
self.lc.Bind(wx.EVT_MOTION, self.OnMotion)
self.lc.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
return True
# Return the widget that is to be used for the popup
def GetControl(self):
return self.lc
# Called just prior to displaying the popup, you can use it to
# 'select' the current item.
def SetStringValue(self, val):
idx = self.lc.FindItem(-1, val)
if idx != wx.NOT_FOUND:
self.lc.Select(idx)
# Return a string representation of the current item.
def GetStringValue(self):
if self.value >= 0:
return self.lc.GetItemText(self.value)
return ""
# Called immediately after the popup is shown
def OnPopup(self):
wx.ComboPopup.OnPopup(self)
# Called when popup is dismissed
def OnDismiss(self):
print (self.GetStringValue())
wx.ComboPopup.OnDismiss(self)
# This is called to custom paint in the combo control itself
# (ie. not the popup). Default implementation draws value as
# string.
def PaintComboControl(self, dc, rect):
wx.ComboPopup.PaintComboControl(self, dc, rect)
# Receives key events from the parent ComboCtrl. Events not
# handled should be skipped, as usual.
def OnComboKeyEvent(self, event):
wx.ComboPopup.OnComboKeyEvent(self, event)
# Implement if you need to support special action when user
# double-clicks on the parent wxComboCtrl.
def OnComboDoubleClick(self):
wx.ComboPopup.OnComboDoubleClick(self)
# Return final size of popup. Called on every popup, just prior to OnPopup.
# minWidth = preferred minimum width for window
# prefHeight = preferred height. Only applies if > 0,
# maxHeight = max height for window, as limited by screen size
# and should only be rounded down, if necessary.
def GetAdjustedSize(self, minWidth, prefHeight, maxHeight):
return wx.ComboPopup.GetAdjustedSize(self, minWidth, prefHeight, maxHeight)
# Return true if you want delay the call to Create until the popup
# is shown for the first time. It is more efficient, but note that
# it is often more convenient to have the control created
# immediately.
# Default returns false.
def LazyCreate(self):
return wx.ComboPopup.LazyCreate(self)
#----------------------------------------------------------------------
class MyTestPanel(wx.Panel):
def __init__(self, parent, log):
self.log = log
wx.Panel.__init__(self, parent, -1)
txt = wx.TextCtrl(self, wx.ID_ANY, pos=(100,100))
comboCtrl = wx.ComboCtrl(self, wx.ID_ANY, "Third item", (10,10), size=(200,-1), style=wx.CB_READONLY)
popupCtrl = ListCtrlComboPopup()
# It is important to call SetPopupControl() as soon as possible
comboCtrl.SetPopupControl(popupCtrl)
# Populate using wx.ListView methods
popupCtrl.AddItem("First Item", [255, 127, 0])
popupCtrl.AddItem("Second Item", [192, 127, 45])
popupCtrl.AddItem("Third Item", [25, 223, 172])
#popupCtrl.GetAdjustedSize(100, 35, 100)
#comboCtrl.SetTextColour(_colour)
comboCtrl.SetForegroundColour(wx.Colour(235, 55, 55))
#----------------------------------------------------------------------
def runTest(frame, nb, log):
win = MyTestPanel(nb, log)
return win
#----------------------------------------------------------------------
overview = """<html><body>
<h2><center>wx.combo.ComboCtrl</center></h2>
A combo control is a generic combobox that allows a totally custom
popup. In addition it has other customization features. For instance,
position and size of the dropdown button can be changed.
</body></html>
"""
if __name__ == '__main__':
import sys,os
import run
run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])
Question 1:
How can I make it so that once an item has been selected the appropriate text colour (the one I programmatically set) and default (gray) background colour is shown.
Question 2:
When dropping down the ComboCtrl, it is showing the ListCtrl, which has a single column only. You can see that the "Second item" on the list is not displayed entirely because the column is too narrow. How can I make it so that the column is always the same width as the widget itself, even when the ComboCtrl resizes (as a result of resizing the parent window)?
Question 3:
Not overly important, but while we are on the subject: is there a way to get rid of the little dotted box that is shown around the selected item when running this in Windows?
In advance, thank you very much for your thoughts and ideas on this.
Marc.

PyQt creating label that follows mouse

What I want to archive is a label that is created as soon as a button is pressed and follows the mouse until there is a 'click'.
My problem with that is that I can't seem to get the 'setMouseTracking(True)' command at the right widget...
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import Qt
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(0,0,1000,1100)
self.main = QtWidgets.QLabel()
self.setCentralWidget(self.main)
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(900, 900)
canvas.fill(QtGui.QColor('#ffffff')) # Fill entire canvas.
self.label.setPixmap(canvas)
# self.last_x, self.last_y = None, None
self.button = QtWidgets.QPushButton('create Block')
self.button.clicked.connect(self.buttonAction)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.label)
vbox.addWidget(self.button)
self.main.setLayout(vbox)
# self.label.setMouseTracking(True)
self.setWindowTitle('testing')
def mouseMoveEvent(self, e):
# if self.last_x is None: # First event.
# self.last_x = e.x()
# self.last_y = e.y()
# return # Ignore the first time.
# painter = QtGui.QPainter(self.label.pixmap())
# painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
# painter.end()
try:
self.image.move(e.x(), e.y())
except:
pass
self.update()
# Update the origin for next time.
# self.last_x = e.x()
# self.last_y = e.y()
def mouseReleaseEvent(self, e):
# self.last_x = None
# self.last_y = None
def buttonAction(self):
block = QtGui.QPixmap(20, 20)
block.fill(QtGui.QColor('blue'))
self.image = QtWidgets.QLabel(self.label)
self.image.setPixmap(block)
self.image.move(20,20)
self.image.show()
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
I don't know if my problem is that I attach setMouseTracking(True) to the wrong widget or if it is something else entirerly.
With clicking action it works, but that is not what I intend to do...
Edit: fixed some of the code issues
For clarification what my problem is: I have a canvas and a button inside an layout, as soon as the button is clicked a new canvas shall be created that follows the mouse pointer 'until' I click. So I don't want any kind of drag-and-drop action, but instead a small canvas that is following the mouse pointer.
This is needed as I intend to use the little canvas to show what an graphic would look like at a certain canvas position without printing it there. So the little canvas is something like a template.
There are some conceptual problems in your logic.
First of all, the mouse tracking only works for the widget it's set on. Also, if the widget accepts the mouse move event, the parent will not receive it.
In your case you are not receiving it because you are implementing the mouseMoveEvent in the main window, which by default ignores it if no mouse button is pressed (like most widgets).
While you could try to set it on the "target" widget and the parent (in your case, the canvas and the main window), you'll certainly have some issues at a certain point if any underlying widget accepts mouse movements; since you're going to need the "preview" only on the actual "canvas", there's no need to create a new widget, as you can just directly paint on the canvas instead, and finally draw on the actual pixmap only when needed.
This is a possible implementation:
class Canvas(QtWidgets.QLabel):
def __init__(self):
super().__init__()
pixmap = QtGui.QPixmap(900, 900)
pixmap.fill(QtCore.Qt.white)
self.setPixmap(pixmap)
self.setMouseTracking(True)
self.preview = False
def startPreview(self):
self.preview = True
self.update()
def drawMiniCanvas(self, pos):
pm = self.pixmap()
qp = QtGui.QPainter(pm)
qp.setBrush(QtCore.Qt.blue)
if self.size() != pm.size():
# if the pixmap is smaller than the actual size of the canvas, the position
# must be translated to its contents before painting
alignment = self.alignment()
pmRect = pm.rect()
if alignment == QtCore.Qt.AlignCenter:
pmRect.moveCenter(self.rect().center())
else:
if alignment & QtCore.Qt.AlignHCenter:
pmRect.moveLeft((self.width() - pm.width()) / 2)
elif alignment & QtCore.Qt.AlignRight:
pmRect.moveRight(self.width())
if alignment & QtCore.Qt.AlignVCenter:
pmRect.moveTop((self.height() - pm.height()) / 2)
elif alignment & QtCore.Qt.AlignBottom:
pmRect.moveBottom(self.height())
pos -= pmRect.topLeft()
qp.drawRect(pos.x(), pos.y(), 20, 20)
qp.end()
self.setPixmap(pm)
def mouseMoveEvent(self, event):
if self.preview:
self.update()
def mousePressEvent(self, event):
if self.preview:
if event.button() == QtCore.Qt.LeftButton:
self.drawMiniCanvas(event.pos())
self.preview = False
def paintEvent(self, event):
super().paintEvent(event)
if self.preview:
qp = QtGui.QPainter(self)
qp.drawRect(self.rect().adjusted(0, 0, -1, -1))
pos = self.mapFromGlobal(QtGui.QCursor.pos())
qp.setBrush(QtCore.Qt.blue)
qp.drawRect(pos.x(), pos.y(), 20, 20)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(0,0,1000,1100)
self.main = QtWidgets.QLabel()
self.setCentralWidget(self.main)
self.canvas = Canvas()
self.button = QtWidgets.QPushButton('create Block')
self.button.clicked.connect(self.canvas.startPreview)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.canvas)
vbox.addWidget(self.button)
self.main.setLayout(vbox)
self.setWindowTitle('testing')
Note that I have left the main widget as a QLabel as per your code, but I strongly suggest to avoid so: QLabel has a complex management of its size, and even if you add a layout to it, the layout requirements will always be ignored; you should use a QWidget instead.
Finally, while the above code works, it's just a simple example based on your question; if you want to create a drawing tool, you should not use a QLabel, and for various reasons: for example, if you want to support scaling to fit the contents, not only the coordinate computation in drawMiniCanvas won't work (due to the scaling), but it will also not paint anything at all, and that's due to the way QLabel caches its contents whenever setScaledContents(True) is used (also, it won't respect the aspect ratio).
For advanced and interactive painting, it's usually better to use a QGraphicsScene shown inside a QGraphicsView.

PyQt5 QTabBar paintEvent with tabs that can move

I would like to have a QTabBar with customised painting in the paintEvent(self,event) method, whilst maintaining the moving tabs animations / mechanics. I posted a question the other day about something similar, but it wasn't worded too well so I have heavily simplified the question with the following code:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtTest import QTest
import sys
class MainWindow(QMainWindow):
def __init__(self,parent=None,*args,**kwargs):
QMainWindow.__init__(self,parent,*args,**kwargs)
self.tabs = QTabWidget(self)
self.tabs.setTabBar(TabBar(self.tabs))
self.tabs.setMovable(True)
for color in ["red","orange","yellow","lime","green","cyan","blue","purple","violet","magenta"]:
title = color
widget = QWidget(styleSheet="background-color:%s" % color)
pixmap = QPixmap(8,8)
pixmap.fill(QColor(color))
icon = QIcon(pixmap)
self.tabs.addTab(widget,icon,title)
self.setCentralWidget(self.tabs)
self.showMaximized()
class TabBar(QTabBar):
def __init__(self,parent,*args,**kwargs):
QTabBar.__init__(self,parent,*args,**kwargs)
def paintEvent(self,event):
painter = QStylePainter(self)
option = QStyleOptionTab()
for i in range(self.count()):
self.initStyleOption(option,i)
#Customise 'option' here
painter.drawControl(QStyle.CE_TabBarTab,option)
def tabSizeHint(self,index):
return QSize(112,48)
def exceptHook(e,v,t):
sys.__excepthook__(e,v,t)
if __name__ == "__main__":
sys.excepthook = exceptHook
application = QApplication(sys.argv)
mainwindow = MainWindow()
application.exec_()
there are some clear problems:
Dragging the tab to 'slide' it in the QTabBar is not smooth (it doens't glide) - it jumps to the next index.
The background tabs (non-selected tabs) don't glide into place once displaced - they jump into position.
When the tab is slid to the end of the tab bar (past the most right tab) and then let go of it doesn't glide back to the last index - it jumps there.
When sliding a tab, it stays in its original place and at the mouse cursor (in its dragging position) at the same time, and only when the mouse is released does the tab only show at the correct place (up until then it is also showing at the index it is originally from).
How can I modify the painting of a QTabBar with a QStyleOptionTab whilst maintaining all of the moving mechanics / animations of the tabs?
While it might seem a slightly simple widget, QTabBar is not, at least if you want to provide all of its features.
If you closely look at its source code, you'll find out that within the mouseMoveEvent() a private QMovableTabWidget is created whenever the drag distance is wide enough. That QWidget is a child of QTabBar that shows a QPixmap grab of the "moving" tab using the tab style option and following the mouse movements, while at the same moment that tab becomes invisible.
While your implementation might seem reasonable (note that I'm also referring to your original, now deleted, question), there are some important issues:
it doesn't account for the above "moving" child widget (in fact, with your code I can still see the original tab, even if that is that moving widget that's not actually moving since no call to the base implementation of mouseMoveEvent() is called);
it doesn't actually tabs;
it doesn't correctly process mouse events;
This is a complete implementation partially based on the C++ sources (I've tested it even with vertical tabs, and it seems to behave as it should):
class TabBar(QTabBar):
class MovingTab(QWidget):
'''
A private QWidget that paints the current moving tab
'''
def setPixmap(self, pixmap):
self.pixmap = pixmap
self.update()
def paintEvent(self, event):
qp = QPainter(self)
qp.drawPixmap(0, 0, self.pixmap)
def __init__(self,parent, *args, **kwargs):
QTabBar.__init__(self,parent, *args, **kwargs)
self.movingTab = None
self.isMoving = False
self.animations = {}
self.pressedIndex = -1
def isVertical(self):
return self.shape() in (
self.RoundedWest,
self.RoundedEast,
self.TriangularWest,
self.TriangularEast)
def createAnimation(self, start, stop):
animation = QVariantAnimation()
animation.setStartValue(start)
animation.setEndValue(stop)
animation.setEasingCurve(QEasingCurve.InOutQuad)
def removeAni():
for k, v in self.animations.items():
if v == animation:
self.animations.pop(k)
animation.deleteLater()
break
animation.finished.connect(removeAni)
animation.valueChanged.connect(self.update)
animation.start()
return animation
def layoutTab(self, overIndex):
oldIndex = self.pressedIndex
self.pressedIndex = overIndex
if overIndex in self.animations:
# if the animation exists, move its key to the swapped index value
self.animations[oldIndex] = self.animations.pop(overIndex)
else:
start = self.tabRect(overIndex).topLeft()
stop = self.tabRect(oldIndex).topLeft()
self.animations[oldIndex] = self.createAnimation(start, stop)
self.moveTab(oldIndex, overIndex)
def finishedMovingTab(self):
self.movingTab.deleteLater()
self.movingTab = None
self.pressedIndex = -1
self.update()
# reimplemented functions
def tabSizeHint(self, i):
return QSize(112, 48)
def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.button() == Qt.LeftButton:
self.pressedIndex = self.tabAt(event.pos())
if self.pressedIndex < 0:
return
self.startPos = event.pos()
def mouseMoveEvent(self,event):
if not event.buttons() & Qt.LeftButton or self.pressedIndex < 0:
super().mouseMoveEvent(event)
else:
delta = event.pos() - self.startPos
if not self.isMoving and delta.manhattanLength() < QApplication.startDragDistance():
# ignore the movement as it's too small to be considered a drag
return
if not self.movingTab:
# create a private widget that appears as the current (moving) tab
tabRect = self.tabRect(self.pressedIndex)
overlap = self.style().pixelMetric(
QStyle.PM_TabBarTabOverlap, None, self)
tabRect.adjust(-overlap, 0, overlap, 0)
pm = QPixmap(tabRect.size())
pm.fill(Qt.transparent)
qp = QStylePainter(pm, self)
opt = QStyleOptionTab()
self.initStyleOption(opt, self.pressedIndex)
if self.isVertical():
opt.rect.moveTopLeft(QPoint(0, overlap))
else:
opt.rect.moveTopLeft(QPoint(overlap, 0))
opt.position = opt.OnlyOneTab
qp.drawControl(QStyle.CE_TabBarTab, opt)
qp.end()
self.movingTab = self.MovingTab(self)
self.movingTab.setPixmap(pm)
self.movingTab.setGeometry(tabRect)
self.movingTab.show()
self.isMoving = True
self.startPos = event.pos()
isVertical = self.isVertical()
startRect = self.tabRect(self.pressedIndex)
if isVertical:
delta = delta.y()
translate = QPoint(0, delta)
startRect.moveTop(startRect.y() + delta)
else:
delta = delta.x()
translate = QPoint(delta, 0)
startRect.moveLeft(startRect.x() + delta)
movingRect = self.movingTab.geometry()
movingRect.translate(translate)
self.movingTab.setGeometry(movingRect)
if delta < 0:
overIndex = self.tabAt(startRect.topLeft())
else:
if isVertical:
overIndex = self.tabAt(startRect.bottomLeft())
else:
overIndex = self.tabAt(startRect.topRight())
if overIndex < 0:
return
# if the target tab is valid, move the current whenever its position
# is over the half of its size
overRect = self.tabRect(overIndex)
if isVertical:
if ((overIndex < self.pressedIndex and movingRect.top() < overRect.center().y()) or
(overIndex > self.pressedIndex and movingRect.bottom() > overRect.center().y())):
self.layoutTab(overIndex)
elif ((overIndex < self.pressedIndex and movingRect.left() < overRect.center().x()) or
(overIndex > self.pressedIndex and movingRect.right() > overRect.center().x())):
self.layoutTab(overIndex)
def mouseReleaseEvent(self,event):
super().mouseReleaseEvent(event)
if self.movingTab:
if self.pressedIndex > 0:
animation = self.createAnimation(
self.movingTab.geometry().topLeft(),
self.tabRect(self.pressedIndex).topLeft()
)
# restore the position faster than the default 250ms
animation.setDuration(80)
animation.finished.connect(self.finishedMovingTab)
animation.valueChanged.connect(self.movingTab.move)
else:
self.finishedMovingTab()
else:
self.pressedIndex = -1
self.isMoving = False
self.update()
def paintEvent(self, event):
if self.pressedIndex < 0:
super().paintEvent(event)
return
painter = QStylePainter(self)
tabOption = QStyleOptionTab()
for i in range(self.count()):
if i == self.pressedIndex and self.isMoving:
continue
self.initStyleOption(tabOption, i)
if i in self.animations:
tabOption.rect.moveTopLeft(self.animations[i].currentValue())
painter.drawControl(QStyle.CE_TabBarTab, tabOption)
I strongly suggest you to carefully read and try to understand the above code (along with the source code), as I didn't comment everything I've done, and it's very important to understand what's happening if you really need to do further subclassing in the future.
Update
If you need to alter the appearance of the dragged tab while moving it, you need to update its pixmap. You can just store the QStyleOptionTab when you create it, and then update when necessary. In the following example the WindowText (note that QPalette.Foreground is obsolete) color is changed whenever the index of the tab is changed:
def mouseMoveEvent(self,event):
# ...
if not self.movingTab:
# ...
self.movingOption = opt
def layoutTab(self, overIndex):
# ...
self.moveTab(oldIndex, overIndex)
pm = QPixmap(self.movingTab.pixmap.size())
pm.fill(Qt.transparent)
qp = QStylePainter(pm, self)
self.movingOption.palette.setColor(QPalette.WindowText, <someColor>)
qp.drawControl(QStyle.CE_TabBarTab, self.movingOption)
qp.end()
self.movingTab.setPixmap(pm)
Another small suggestion: while you can obviously use the indentation style you like, when sharing your code on public spaces like StackOverflow it's always better to stick to common conventions, so I suggest you to always provide your code with 4-spaces indentations; also, remember that there should always be a space after each comma separated variable, as it dramatically improves readability.

Python PyQt5 - Is it possible to add a Button to press inside QTreeView?

I want to add a QPushButton to the tree view that ends with .pdf and when I click it I want to return the path for that Index it's assigned at.
This might not even be possible with the Native QTreeView but if anyone could guide me in the right direction that would be awesome!
To conclude more of what I would want is to have a QPushButton appear where that red square is below.
Current code for the "Tree View":
from PyQt5.QtMultimediaWidgets import *
from PyQt5.QtMultimedia import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5 import *
import os, sys
class MainMenu(QWidget):
def __init__(self, parent = None):
super(MainMenu, self).__init__(parent)
self.model = QFileSystemModel()
self.model.setRootPath(QDir.rootPath())
self.model.setFilter(QDir.NoDotAndDotDot | QDir.AllEntries | QDir.Dirs | QDir.Files)
self.proxy_model = QSortFilterProxyModel(recursiveFilteringEnabled = True, filterRole = QFileSystemModel.FileNameRole)
self.proxy_model.setSourceModel(self.model)
self.model.setReadOnly(False)
self.model.setNameFilterDisables(False)
self.indexRoot = self.model.index(self.model.rootPath())
self.treeView = QTreeView(self)
self.treeView.setModel(self.proxy_model)
self.treeView.setRootIndex(self.indexRoot)
self.treeView.setAnimated(True)
self.treeView.setIndentation(20)
self.treeView.setSortingEnabled(True)
self.treeView.setDragEnabled(False)
self.treeView.setAcceptDrops(False)
self.treeView.setDropIndicatorShown(True)
self.treeView.setEditTriggers(QTreeView.NoEditTriggers)
for i in range(1, self.treeView.model().columnCount()):
self.treeView.header().hideSection(i)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainMenu()
main.show()
sys.exit(app.exec_())
For this you'll probably need an item delegate.
The idea is that you are going to leave basic item painting to the base class paint() function, and then paint a virtual button over it.
To achieve that, QStyleOptionButton is used against the view style (obtained from the option argument): you create a style option, init it from the view (option.widget, which will apply the basic rectangle of the widget, the font, palette, etc.), adjust the rectangle to suit your needs and finally paint it.
To better implement drawing (mouse hover effects, but also to ensure correct painting update), you'll also need to set mouse tracking to True for the tree view. This, amongst other checks explained in the code, allows you to draw the virtual button, including its hover or pressed states.
Finally, when the button is released and the mouse is within its boundaries, a buttonClicked signal is emitted, with the current index as argument.
class TreeButtonDelegate(QtWidgets.QStyledItemDelegate):
buttonClicked = QtCore.pyqtSignal(QtCore.QModelIndex, int)
def __init__(self, fsModel, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fsModel = fsModel
self.clickedPaths = {}
self._mousePos = None
self._pressed = False
self.minimumButtonWidth = 32
def getOption(self, option, index):
btnOption = QtWidgets.QStyleOptionButton()
# initialize the basic options with the view
btnOption.initFrom(option.widget)
clickedCount = self.clickedPaths.get(self.fsModel.filePath(index), 0)
if clickedCount:
btnOption.text = '{}'.format(clickedCount)
else:
btnOption.text = 'NO'
# the original option properties should never be touched, so we can't
# directly use it's "rect"; let's create a new one from it
btnOption.rect = QtCore.QRect(option.rect)
# adjust it to the minimum size
btnOption.rect.setLeft(option.rect.right() - self.minimumButtonWidth)
style = option.widget.style()
# get the available space for the contents of the button
textRect = style.subElementRect(
QtWidgets.QStyle.SE_PushButtonContents, btnOption)
# get the margins between the contents and the border, multiplied by 2
# since they're used for both the left and right side
margin = style.pixelMetric(
QtWidgets.QStyle.PM_ButtonMargin, btnOption) * 2
# the width of the current button text
textWidth = btnOption.fontMetrics.width(btnOption.text)
if textRect.width() < textWidth + margin:
# if the width is too small, adjust the *whole* button rect size
# to fit the contents
btnOption.rect.setLeft(btnOption.rect.left() - (
textWidth - textRect.width() + margin))
return btnOption
def editorEvent(self, event, model, option, index):
# map the proxy index to the fsModel
srcIndex = index.model().mapToSource(index)
# I'm just checking if it's a file, if you want to check the extension
# you might need to use fsModel.fileName(srcIndex)
if not self.fsModel.isDir(srcIndex):
if event.type() in (QtCore.QEvent.Enter, QtCore.QEvent.MouseMove):
self._mousePos = event.pos()
# request an update of the current index
option.widget.update(index)
elif event.type() == QtCore.QEvent.Leave:
self._mousePos = None
elif (event.type() in (QtCore.QEvent.MouseButtonPress, QtCore.QEvent.MouseButtonDblClick)
and event.button() == QtCore.Qt.LeftButton):
# check that the click is within the virtual button rectangle
if event.pos() in self.getOption(option, srcIndex).rect:
self._pressed = True
option.widget.update(index)
if event.type() == QtCore.QEvent.MouseButtonDblClick:
# do not send double click events
return True
elif event.type() == QtCore.QEvent.MouseButtonRelease:
if self._pressed and event.button() == QtCore.Qt.LeftButton:
# emit the click only if the release is within the button rect
if event.pos() in self.getOption(option, srcIndex).rect:
filePath = self.fsModel.filePath(srcIndex)
count = self.clickedPaths.setdefault(filePath, 0)
self.buttonClicked.emit(index, count + 1)
self.clickedPaths[filePath] += 1
self._pressed = False
option.widget.update(index)
return super().editorEvent(event, model, option, index)
def paint(self, painter, option, index):
super().paint(painter, option, index)
srcIndex = index.model().mapToSource(index)
if not self.fsModel.isDir(srcIndex):
btnOption = self.getOption(option, srcIndex)
# remove the focus rectangle, as it will be inherited from the view
btnOption.state &= ~QtWidgets.QStyle.State_HasFocus
if self._mousePos is not None and self._mousePos in btnOption.rect:
# if the style supports it, some kind of "glowing" border
# will be shown on the button
btnOption.state |= QtWidgets.QStyle.State_MouseOver
if self._pressed == QtCore.Qt.LeftButton:
# set the button pressed state
btnOption.state |= QtWidgets.QStyle.State_On
else:
# ensure that there's no mouse over state (see above)
btnOption.state &= ~QtWidgets.QStyle.State_MouseOver
# finally, draw the virtual button
option.widget.style().drawControl(
QtWidgets.QStyle.CE_PushButton, btnOption, painter)
class MainMenu(QWidget):
def __init__(self, parent = None):
super(MainMenu, self).__init__(parent)
# ...
self.treeView = QTreeView(self)
self.treeView.setMouseTracking(True)
# ...
self.treeDelegate = TreeDelegate(self.model)
self.treeView.setItemDelegateForColumn(0, self.treeDelegate)
self.treeDelegate.buttonClicked.connect(self.treeButtonClicked)
# ...
def treeButtonClicked(self, index, count):
print('{} clicked {} times'.format(index.data(), count))
Note: I implemented the click counter as you asked in the comments (and used an helper function to accomodate the longer function that computes the button size accordingly), just remember that this doesn't take into account the possibility of files renamed, removed and/or recreated (or files renamed overwriting an existing one). To obtain that you'll need to use a more complex approach than a simple path-based dictionary, possibly by implementing QFileSystemWatcher and checking for files removed/renamed.
Also note that to speed up things a bit I'm adding the source filesystem model to the init of the delegate so that it doesn't need to be found each time it's required for painting or mouse tracking.

Drag n Drop inside QgraphicsView doesn't work (PyQt)

I created a custom class for some buttons. Those are a "draggable" buttons, which its name indicates, are buttons that you can drag and drop into each other (depending if is allowDrag property is set) and then make an action.
The code of those dragbuttons is already posted here:
Drag n Drop Button and Drop-down menu PyQt/Qt designer
Apparently the buttons work well when they are in a QWidget, but when they are added into a scene of a QGraphicsView (I also made a custom class of it) the drop event doesn't work. I get a QGraphicsItem::ungrabMouse: not a mouse grabber warning instead.
This is the code for the custom GraphicsView:
from PyQt4 import QtGui, QtCore
class WiringGraphicsView(QtGui.QGraphicsView):
#Initializer method
def __init__(self, parent = None, scene=None):
QtGui.QGraphicsView.__init__(self, scene, parent)
#Set Accept Drops property true
self.setAcceptDrops(True)
#This method creates a line between two widgets
def paintWire(self, start_widget, end_widget):
#Size and Position of both widgets
_start = start_widget.geometry()
_end = end_widget.geometry()
#Creates a Brush object with Red color
brush = QtGui.QBrush(QtGui.QColor(255, 0, 0) )
#Creates Pen object with specified brush
pen = QtGui.QPen(brush, 2)
#Create a Line object between two widgets
line = QtGui.QGraphicsLineItem(_start.x() + _start.width() / 2, _start.y() + _start.height() / 2, _end.x() + _end.width() / 2, _end.y() + _end.height() / 2)
#Set the Pen for the Line
line.setPen(pen)
#Add this line item to the scene.
self.scene().addItem( line )
And here the code where the custom buttons and the graphicsView are:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from dragbutton import DragButton
from wiringgraphicsview import WiringGraphicsView
import icons_rc
app = QApplication([])
scene = QGraphicsScene()
menu = QMenu()
# put a button into the scene and move it
button1 = DragButton('Button 1')
button1.setText("")
button1.setDefault(False)
button1.setAutoDefault(True)
#button1.setMouseTracking(True)
button1.setAllowDrag(True) #Allow Drag n Drop of DragButton
button1.setGeometry(QRect(50, 50, 51, 31)) #Set dimensions of it
#Set icon of button1
icon = QIcon()
icon.addPixmap(QPixmap(":/audio-input-line.png"), QIcon.Normal, QIcon.Off)
button1.setIcon(icon)
button1.setFlat(True)
button1.setMenu(menu)
#Create a QGraphicsProxyWidget adding the widget to scene
scene_button1 = scene.addWidget(button1)
#move the button on the scene
r1 = scene_button1.geometry()
r1.moveTo(-100, -50)
# put another button into the scene
button2 = DragButton('Button 2')
button2.setText("")
#This button shoudn't be dragged, it is just for dropping.
button2.setAllowDrag(False)
button2.setAcceptDrops(True)
icon = QIcon()
icon.addPixmap(QPixmap(":/input_small.png"), QIcon.Normal, QIcon.Off)
button2.setIcon(icon)
#button2.setMouseTracking(True)
#button2.setGeometry(QRect(270, 150, 41, 31))
scene_button2 = scene.addWidget(button2)
scene_button2.setAcceptDrops(True)
r2 = scene_button2.geometry()
# Create the view using the scene
view = WiringGraphicsView(None, scene)
view.resize(300, 200)
view.show()
#and paint a wire between those buttons
view.paintWire(button1, button2)
app.exec_()
Plus: what about if I want to embed buttons into a horizontal or vertical layout first (to have them in order) and then into a QgraphicsView, is that possible?
EDIT: I already figured out that you can add layouts with their child buttons into a graphicscene as any other widget.
I still don't know why my drag n drop implemented in my dragbutton class is not working when is inside of a Qgraphicsscene/QgraphicsView.
Most of the documentation that I read talks about to implement drag n drop logic but in a QgraphicsItem class.
It would be a good idea to create a new class based on QGraphicsItem, but at this point makes me to do the following questions:
How I suppose to re-implement the button behaviour? Click effects, properties, and the possibility to add a QMenu? This already works when I useaddWidget to add a QButton or my custom DragButton into a scene.
What about the layouts? I can't add QgraphicsItem into a layout and then add the layout to the scene! Is there way to have those items in order when they are in a scene/view?
EDIT 2: I included the code of the "DragButton" class posted in my other post, since is relevant to this question.
from PyQt4 import QtGui, QtCore
class DragButton(QtGui.QPushButton):
def __init__(self, parent):
super(DragButton, self).__init__(parent)
self.allowDrag = True
def setAllowDrag(self, allowDrag):
if type(allowDrag) == bool:
self.allowDrag = allowDrag
else:
raise TypeError("You have to set a boolean type")
def mouseMoveEvent(self, e):
if e.buttons() != QtCore.Qt.RightButton:
return
if self.allowDrag == True:
# write the relative cursor position to mime data
mimeData = QtCore.QMimeData()
# simple string with 'x,y'
mimeData.setText('%d,%d' % (e.x(), e.y()))
print mimeData.text()
# let's make it fancy. we'll show a "ghost" of the button as we drag
# grab the button to a pixmap
pixmap = QtGui.QPixmap.grabWidget(self)
# below makes the pixmap half transparent
painter = QtGui.QPainter(pixmap)
painter.setCompositionMode(painter.CompositionMode_DestinationIn)
painter.fillRect(pixmap.rect(), QtGui.QColor(0, 0, 0, 127))
painter.end()
# make a QDrag
drag = QtGui.QDrag(self)
# put our MimeData
drag.setMimeData(mimeData)
# set its Pixmap
drag.setPixmap(pixmap)
# shift the Pixmap so that it coincides with the cursor position
drag.setHotSpot(e.pos())
# start the drag operation
# exec_ will return the accepted action from dropEvent
if drag.exec_(QtCore.Qt.LinkAction | QtCore.Qt.MoveAction) == QtCore.Qt.LinkAction:
print 'linked'
else:
print 'moved'
def mousePressEvent(self, e):
QtGui.QPushButton.mousePressEvent(self, e)
if e.button() == QtCore.Qt.LeftButton:
print 'press'
#AQUI DEBO IMPLEMENTAR EL MENU CONTEXTUAL
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
# get the relative position from the mime data
mime = e.mimeData().text()
x, y = map(int, mime.split(','))
# move
# so move the dragged button (i.e. event.source())
print e.pos()
#e.source().move(e.pos()-QtCore.QPoint(x, y))
# set the drop action as LinkAction
e.setDropAction(QtCore.Qt.LinkAction)
# tell the QDrag we accepted it
e.accept()
The solution appears to require that you subclass QGraphicsScene to explicitly pass the drop events to the QGraphicsItem at the drop coordinates. Further more, QGraphicsProxyWidget does not appear to pass drop events to the child widget. So again, you need to subclass QGraphicsProxyWidget and manually instantiate this class, add the widget and hen manually add the instance to the scene using scene.addItem().
Note: You are probably aware, but you the drag/drop isn't started unless you have first interacted with the widget (e.g. clicked on it). Presumably this could be fixed by also passing through the mouseMoveEvent from the scene to the proxy and then to the widget.
Note 2: I don't know why it takes so much effort to make this work. I do feel like I may be missing something. The documentation says:
QGraphicsProxyWidget supports all core features of QWidget, including tab focus, keyboard input, Drag & Drop, and popups
but I couldn't make it work without subclassing.
Relevant subclass implementations:
class MyScene(QGraphicsScene):
def dragEnterEvent(self, e):
e.acceptProposedAction()
def dropEvent(self, e):
# find item at these coordinates
item = self.itemAt(e.scenePos())
if item.setAcceptDrops == True:
# pass on event to item at the coordinates
try:
item.dropEvent(e)
except RuntimeError:
pass #This will supress a Runtime Error generated when dropping into a widget with no MyProxy
def dragMoveEvent(self, e):
e.acceptProposedAction()
class MyProxy(QGraphicsProxyWidget):
def dragEnterEvent(self, e):
e.acceptProposedAction()
def dropEvent(self, e):
# pass drop event to child widget
return self.widget().dropEvent(e)
def dragMoveEvent(self, e):
e.acceptProposedAction()
Modified application code:
scene = MyScene()
...
my_proxy = MyProxy()
my_proxy.setWidget(button2)
my_proxy.setAcceptDrops(True)
scene.addItem(my_proxy)
...
Full working (well, it prints out "linked" when the drag drop succeeds...which is all you had written it to do previously) application:
from PyQt4 import QtGui, QtCore
class WiringGraphicsView(QtGui.QGraphicsView):
#Initializer method
def __init__(self, parent = None, scene=None):
QtGui.QGraphicsView.__init__(self, scene, parent)
#Set Accept Drops property true
self.setAcceptDrops(True)
#This method creates a line between two widgets
def paintWire(self, start_widget, end_widget):
#Size and Position of both widgets
_start = start_widget.geometry()
_end = end_widget.geometry()
#Creates a Brush object with Red color
brush = QtGui.QBrush(QtGui.QColor(255, 0, 0) )
#Creates Pen object with specified brush
pen = QtGui.QPen(brush, 2)
#Create a Line object between two widgets
line = QtGui.QGraphicsLineItem(_start.x() + _start.width() / 2, _start.y() + _start.height() / 2, _end.x() + _end.width() / 2, _end.y() + _end.height() / 2)
#Set the Pen for the Line
line.setPen(pen)
#Add this line item to the scene.
self.scene().addItem( line )
class DragButton(QtGui.QPushButton):
def __init__(self, parent):
super(DragButton, self).__init__(parent)
self.allowDrag = True
def setAllowDrag(self, allowDrag):
if type(allowDrag) == bool:
self.allowDrag = allowDrag
else:
raise TypeError("You have to set a boolean type")
def mouseMoveEvent(self, e):
if e.buttons() != QtCore.Qt.RightButton:
return QtGui.QPushButton.mouseMoveEvent(self, e)
if self.allowDrag == True:
# write the relative cursor position to mime data
mimeData = QtCore.QMimeData()
# simple string with 'x,y'
mimeData.setText('%d,%d' % (e.x(), e.y()))
# print mimeData.text()
# let's make it fancy. we'll show a "ghost" of the button as we drag
# grab the button to a pixmap
pixmap = QtGui.QPixmap.grabWidget(self)
# below makes the pixmap half transparent
painter = QtGui.QPainter(pixmap)
painter.setCompositionMode(painter.CompositionMode_DestinationIn)
painter.fillRect(pixmap.rect(), QtGui.QColor(0, 0, 0, 127))
painter.end()
# make a QDrag
drag = QtGui.QDrag(self)
# put our MimeData
drag.setMimeData(mimeData)
# set its Pixmap
drag.setPixmap(pixmap)
# shift the Pixmap so that it coincides with the cursor position
drag.setHotSpot(e.pos())
# start the drag operation
# exec_ will return the accepted action from dropEvent
if drag.exec_(QtCore.Qt.LinkAction | QtCore.Qt.MoveAction) == QtCore.Qt.LinkAction:
print 'linked'
else:
print 'moved'
return QtGui.QPushButton.mouseMoveEvent(self, e)
def mousePressEvent(self, e):
if e.button() == QtCore.Qt.LeftButton:
print 'press'
#AQUI DEBO IMPLEMENTAR EL MENU CONTEXTUAL
return QtGui.QPushButton.mousePressEvent(self, e)
def dragEnterEvent(self, e):
e.accept()
return QtGui.QPushButton.dragEnterEvent(self, e)
def dropEvent(self, e):
# get the relative position from the mime data
mime = e.mimeData().text()
x, y = map(int, mime.split(','))
# move
# so move the dragged button (i.e. event.source())
print e.pos()
# e.source().move(e.pos()-QtCore.QPoint(x, y))
# set the drop action as LinkAction
e.setDropAction(QtCore.Qt.LinkAction)
# tell the QDrag we accepted it
e.accept()
return QtGui.QPushButton.dropEvent(self, QDropEvent(QPoint(e.pos().x(), e.pos().y()), e.possibleActions(), e.mimeData(), e.buttons(), e.modifiers()))
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MyScene(QGraphicsScene):
def dragEnterEvent(self, e):
e.acceptProposedAction()
def dropEvent(self, e):
# find item at these coordinates
item = self.itemAt(e.scenePos())
if item.setAcceptDrops == True:
# pass on event to item at the coordinates
try:
item.dropEvent(e)
except RuntimeError:
pass #This will supress a Runtime Error generated when dropping into a widget with no ProxyWidget
def dragMoveEvent(self, e):
e.acceptProposedAction()
class MyProxy(QGraphicsProxyWidget):
def dragEnterEvent(self, e):
e.acceptProposedAction()
def dropEvent(self, e):
# pass drop event to child widget
return self.widget().dropEvent(e)
def dragMoveEvent(self, e):
e.acceptProposedAction()
app = QApplication([])
scene = MyScene()
menu = QMenu()
# put a button into the scene and move it
button1 = DragButton('Button 1')
button1.setText("aaa")
button1.setDefault(False)
button1.setAutoDefault(True)
#button1.setMouseTracking(True)
button1.setAllowDrag(True) #Allow Drag n Drop of DragButton
button1.setGeometry(QRect(50, 50, 51, 31)) #Set dimensions of it
#Set icon of button1
icon = QIcon()
icon.addPixmap(QPixmap(":/audio-input-line.png"), QIcon.Normal, QIcon.Off)
button1.setIcon(icon)
button1.setFlat(True)
button1.setMenu(menu)
#Create a QGraphicsProxyWidget adding the widget to scene
scene_button1 = scene.addWidget(button1)
#move the button on the scene
r1 = scene_button1.geometry()
r1.moveTo(-100, -50)
# put another button into the scene
button2 = DragButton('Button 2')
button2.setText("bbb")
#This button shoudn't be dragged, it is just for dropping.
button2.setAllowDrag(False)
button2.setAcceptDrops(True)
icon = QIcon()
icon.addPixmap(QPixmap(":/input_small.png"), QIcon.Normal, QIcon.Off)
button2.setIcon(icon)
#button2.setMouseTracking(True)
#button2.setGeometry(QRect(270, 150, 41, 31))
# Instantiate our own proxy which forwars drag/drop events to the child widget
my_proxy = MyProxy()
my_proxy.setWidget(button2)
my_proxy.setAcceptDrops(True)
scene.addItem(my_proxy)
# Create the view using the scene
view = WiringGraphicsView(None, scene)
view.resize(300, 200)
view.show()
#and paint a wire between those buttons
view.paintWire(button1, button2)
app.exec_()

Categories

Resources