I have a QGraphicsScene and its associated QGraphicsView. I let the user create some shapes in the form of derived QGraphicsItems into that scene. I also want them to be movable by mouse. Clicking one or more items select them, and moving them around while the mouse button is pressed works. I inherited QGraphicsView to do this, and overrided mousePressedEvent, mouseReleasedEvent & mouseMoveEvent to achieve this. When the user clicks, I am basically testing if an item (accessed through items() which returns the items of the associated scene) is under the mouse with contains(), and if it is then I am selecting it.
In the mouseMoveEvent, I am using setPos() on each item of the selection to move it relatively to the mouse move. It works and displays as expected.
This may not be the most efficient way, but that's what I achieved while discovering Qt. Now, the problem is : once I've moved my item or group of items with the mouse, if I want to deselect them (by clicking again on them), the contains() method supplied with the position of the input acts as if the item wasn't moved. Example : I draw a rectangle in the upper left corner, and move it around to, say, the center of the view. Clicking on it again doesn't work but clicking on where it was initially works. So I suspect it has something to do with local and global coordinates.
I've run through several problems today (most of them resolved) but I'm stuck on this one.
Here's my View class :
class CustomGraphicsView(QGraphicsView):
def __init__(self, *args):
super().__init__(*args)
self.selection = []
self.offsets = []
self.select_point = None
def mousePressEvent(self, event):
pos = self.mapFromGlobal(event.globalPos())
modifiers = event.modifiers()
if event.button() == Qt.LeftButton:
#do something else
elif event.button() == Qt.RightButton:
self.select_point = pos
for s in self.selection:
if s.contains(pos): # deselect or drag
for s in self.selection: # construct the offsets for dragging
self.offsets = [s.pos() - pos for s in self.selection]
break
def mouseReleaseEvent(self, event):
pos = self.mapFromGlobal(event.globalPos())
modifiers = event.modifiers()
if event.button() == Qt.LeftButton:
#do something else
elif event.button() == Qt.RightButton:
if self.select_point == pos: # one click selection
self.update_selection(pos)
if self.offsets:
self.offsets.clear()
def mouseMoveEvent(self, event):
pos = self.mapFromGlobal(event.globalPos())
modifiers = event.modifiers()
if event.buttons() == Qt.RightButton:
if not self.offsets:
for s in self.selection:
self.offsets = [s.pos() - pos for s in self.selection]
for s, off in zip(self.selection, self.offsets):
s.set_pos(pos + off)
def update_selection(self, pos):
for item in self.items():
if not item.contains(pos):
continue
if item.selected:
self.selection.remove(item)
else:
self.selection.append(item)
item.select()
break
The scene rect is set at (0;0) so there's no concern about moving it or whatever.
Related
Currently, QTextEdit permits selecting text and then altering that selection with shift-click-drag only on the side of the selection opposite the anchor. The anchor is placed where the selection started. If the user tries to alter the selection near the start, the selection pivots around the anchor point instead of extending. I'd like to permit changing the selection from either side.
My first attempt is to simply set the anchor on the opposite side from where the cursor is located. Say, for example, the selection is from 10 to 20. If the cursor is shift-click-dragged at position 8, then the anchor would be set to 20. If the cursor is shift-click-dragged at position 22, then the anchor would be set to 10. Later, I'll try something more robust, perhaps based on the center point of the selection.
I thought this code would work, but it does not seem to affect the default behavior at all. What have I missed?
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class TextEditor(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
self.setMouseTracking(True)
def mouseMoveEvent(self, event):
point = QPoint()
x = event.x() #these are relative to the upper left corner of the text edit window
y = event.y()
point.setX(x)
point.setY(y)
self.mousepos = self.cursorForPosition(point).position() # get character position of current mouse using local window coordinates
if event.buttons()==Qt.LeftButton:
modifiers = QApplication.keyboardModifiers()
if modifiers == Qt.ShiftModifier:
start = -1 #initialize to something impossible
end = -1
cursor = self.textCursor()
select_point1 = cursor.selectionStart()
select_point2 = cursor.selectionEnd()
if select_point1 < select_point2: # determine order of selection points
start = select_point1
end = select_point2
elif select_point2 < select_point1:
start = select_point2
end = select_point1
if self.mousepos > end: # if past end when shift-click then trying to extend right
cursor.setPosition(start, mode=QTextCursor.MoveAnchor)
elif self.mousepos < start: # if before start when shift-click then trying to extend left
cursor.setPosition(end, mode=QTextCursor.MoveAnchor)
if start != -1 and end != -1: #if selection exists then this should trigger
self.setTextCursor(cursor)
super().mouseMoveEvent(event)
Here's a first stab at implementing shift+click extension of the current selection. It seems to work okay, but I have not tested it to death, so there may be one or two glitches. The intended behaviour is that a shift+click above or below the selection should extend the whole selection in that direction; and a shift+click with drag should do the same thing, only continuously.
Note that I have also set the text-interaction flags so that the caret is visible in read-only mode, and the selection can also be manipulated with the keyboard in various ways (e.g. ctrl+shift+right extends the selection to the next word).
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class TextEditor(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
self.setTextInteractionFlags(
Qt.TextSelectableByMouse |
Qt.TextSelectableByKeyboard)
def mouseMoveEvent(self, event):
if not self.setShiftSelection(event, True):
super().mouseMoveEvent(event)
def mousePressEvent(self, event):
if not self.setShiftSelection(event):
super().mousePressEvent(event)
def setShiftSelection(self, event, moving=False):
if (event.buttons() == Qt.LeftButton and
QApplication.keyboardModifiers() == Qt.ShiftModifier):
cursor = self.textCursor()
start = cursor.selectionStart()
end = cursor.selectionEnd()
if not moving or start != end:
anchor = cursor.anchor()
pos = self.cursorForPosition(event.pos()).position()
if pos <= start:
start = pos
elif pos >= end:
end = pos
elif anchor == start:
end = pos
else:
start = pos
if pos <= anchor:
start, end = end, start
cursor.setPosition(start, QTextCursor.MoveAnchor)
cursor.setPosition(end, QTextCursor.KeepAnchor)
self.setTextCursor(cursor)
return True
return False
if __name__ == '__main__':
app = QApplication(sys.argv)
window = TextEditor()
window.setText(open(__file__).read())
window.setGeometry(600, 50, 800, 800)
window.show()
sys.exit(app.exec_())
I'm trying to click and drag to zoom in and out of a QGraphicsView like you see in graphics applications like Maya and Nuke. There is a lot of information about using the mouse wheel but I haven't found anything related to dragging to zoom.
Is there an easy way to do this or do I need to roll my own implementation of the "anchor" effect?
The following will work but the view follows the mouse around as I drag the zoom rather than appearing to zoom in and out of a fixed point in space (the point where the mouse was clicked to start the drag-zoom.
(This is a bunch of copy and paste from my more complex source code. It is intended to be illustrative though it should run)
def mousePressEvent(self, event):
self.press_mouse_pos = event.pos()
transform = self.transform()
self.press_translate = [transform.m31(), transform.m32()]
self.press_scale = transform.m11()
if event.button() == QtCore.Qt.RightButton and \
event.modifiers() == QtCore.Qt.AltModifier:
self.scaling = True
event.accept()
else:
super(GraphView, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.scaling:
delta_pos = event.pos() - self.press_mouse_pos
amount = delta_pos.x() + delta_pos.y()
speed = 0.001
scl = self.press_scale - (amount * speed)
scl = min(1.0, max(scl, 0.1)) # Clamp so we don't go to far in or out
transform = QtGui.QTransform(
scl, 0, 0,
0, scl, 0,
self.press_translate[0], self.press_translate[1], 1
)
# If interactive is True then some double calculations are triggered
prev_interactive_state = self.isInteractive()
prev_anchor_mode = self.transformationAnchor()
self.setInteractive(False)
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setTransform(transform)
self.setInteractive(prev_interactive_state)
self.setTransformationAnchor(prev_anchor_mode)
else:
super(GraphView, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.scaling = False
super(GraphView, self).mouseReleaseEvent(event)
Change this:
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
to that:
self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
setTransformation() is a subject to transformation anchor; AnchorUnderMouse makes sure that a scene point corresponding the current (at the moment when transform is applied) mouse position remains untranslated.
I have QTableWidget with CheckBoxes in some cells. I want to disable user to perform mouse click over the table cells (so he can't change checkBox state) for some time while I am using data from the table. I've tried table.setDisabled(1) but that disables whole table and I need scroll to be enabled.
Any help would be appreciated.
EDIT
To be more precise: there could be up to 15x3000 cells in table, filled with text(editable), checkbox(checkable), svg graphic(opens other window when double click on it) or some custom widgets(which also have clickable or editable parts). I need to disable user to click or double click over cells(so he can't change any of them) for 1sec - 10sec time interval (solution must be something fast, not iterating through all items), but I need scroll-bar to be enabled and normal table visibility.
One way to achieve this is to subclass QTableWidgetItem and re-implement the setData method. That way, you can control whether items accept values for certain roles.
To control the "checkability" for all items, you could add a class attribute to the subclass which could be tested whenever a value for the check-state role was passed to setData.
Here's what the subclass might look like:
class TableWidgetItem(QtGui.QTableWidgetItem):
_blocked = True
#classmethod
def blocked(cls):
return cls._checkable
#classmethod
def setBlocked(cls, checkable):
cls._checkable = bool(checkable)
def setData(self, role, value):
if role != QtCore.Qt.CheckStateRole or self.checkable():
QtGui.QTableWidgetItem.setData(self, role, value)
And the "checkability" of all items would be disabled like this:
TableWidgetItem.setCheckable(False)
UPDATE:
The above idea can be extended by adding a generic wrapper class for cell widgets.
The classes below will block changes to text and check-state for table-widget items, and also a range of keyboard and mouse events for cell widgets via an event-filter (other events can be blocked as required).
The cell-widgets would need to be created like this:
widget = CellWidget(self.table, QtGui.QLineEdit())
self.table.setCellWidget(row, column, widget)
and then accessed like this:
widget = self.table.cellWidget().widget()
Blocking for the whole table would be switched on like this:
TableWidgetItem.setBlocked(True)
CellWidget.setBlocked(True)
# or Blockable.setBlocked(True)
Here are the classes:
class Blockable(object):
_blocked = False
#classmethod
def blocked(cls):
return cls._blocked
#classmethod
def setBlocked(cls, blocked):
cls._blocked = bool(blocked)
class TableWidgetItem(Blockable, QtGui.QTableWidgetItem):
def setData(self, role, value):
if (not self.blocked() or (
role != QtCore.Qt.EditRole and
role != QtCore.Qt.CheckStateRole)):
QtGui.QTableWidgetItem.setData(self, role, value)
class CellWidget(Blockable, QtGui.QWidget):
def __init__(self, parent, widget):
QtGui.QWidget.__init__(self, parent)
self._widget = widget
layout = QtGui.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(widget)
widget.setParent(self)
widget.installEventFilter(self)
if hasattr(widget, 'viewport'):
widget.viewport().installEventFilter(self)
widget.show()
def widget(self):
return self._widget
def eventFilter(self, widget, event):
if self.blocked():
etype = event.type()
if (etype == QtCore.QEvent.KeyPress or
etype == QtCore.QEvent.KeyRelease or
etype == QtCore.QEvent.MouseButtonPress or
etype == QtCore.QEvent.MouseButtonRelease or
etype == QtCore.QEvent.MouseButtonDblClick or
etype == QtCore.QEvent.ContextMenu or
etype == QtCore.QEvent.Wheel):
return True
return QtGui.QWidget.eventFilter(self, widget, event)
Just iterate through all QStandardItems and change flags values for items which should not be changeable.
You can use flag: Qt::ItemIsEditable or/and Qt::ItemIsEnabled.
You would need to disable the items themselves as opposed to the whole table if you have other items than QCheckBoxes that you would not like to disable. See the python code below for details:
'''
Iterate through all the check boxes in the standard items
and make sure the enabled flag is cleared so that the items are disabled
'''
for standardItem in standardItems:
standardItem.setFlags(standardItem.flags() & ~Qt.ItemIsEnabled)
Here you can find the corresponding documentation:
void QTableWidgetItem::setFlags(Qt::ItemFlags flags)
Sets the flags for the item to the given flags. These determine whether the item can be selected or modified.
I'm building in PyQt4 and can't figure out how to add text to a QGraphicsPolygonItem. The idea is to have text set in the middle of a rectangular box after a user double clicks (and gets a dialog box via QInputDialog.getText).
The class is:
class DiagramItem(QtGui.QGraphicsPolygonItem):
def __init__(self, diagramType, contextMenu, parent=None, scene=None):
super(DiagramItem, self).__init__(parent, scene)
path = QtGui.QPainterPath()
rect = self.outlineRect()
path.addRoundRect(rect, self.roundness(rect.width()), self.roundness(rect.height()))
self.myPolygon = path.toFillPolygon()
My double mouse click event looks like this, but updates nothing!
def mouseDoubleClickEvent(self, event):
text, ok = QtGui.QInputDialog.getText(QtGui.QInputDialog(),'Create Region Title','Enter Region Name: ', \
QtGui.QLineEdit.Normal, 'region name')
if ok:
self.myText = str(text)
pic = QtGui.QPicture()
qp = QtGui.QPainter(pic)
qp.setFont(QtGui.QFont('Arial', 40))
qp.drawText(10,10,200,200, QtCore.Qt.AlignCenter, self.myText)
qp.end()
Well, you are not doing it correctly. You are painting to a QPicture (pic) and throwing it away.
I'm assuming you want to paint on the QGraphicsPolygonItem. paint method of QGraphicsItem (and its derivatives) is responsible for painting the item. If you want to paint extra things with the item, you should override that method and do your painting there:
class DiagramItem(QtGui.QGraphicsPolygonItem):
def __init__(self, diagramType, contextMenu, parent=None, scene=None):
super(DiagramItem, self).__init__(parent, scene)
# your `init` stuff
# ...
# just initialize an empty string for self.myText
self.myText = ''
def mouseDoubleClickEvent(self, event):
text, ok = QtGui.QInputDialog.getText(QtGui.QInputDialog(),
'Create Region Title',
'Enter Region Name: ',
QtGui.QLineEdit.Normal,
'region name')
if ok:
# you can leave it as QString
# besides in Python 2, you'll have problems with unicode text if you use str()
self.myText = text
# force an update
self.update()
def paint(self, painter, option, widget):
# paint the PolygonItem's own stuff
super(DiagramItem, self).paint(painter, option, widget)
# now paint your text
painter.setFont(QtGui.QFont('Arial', 40))
painter.drawText(10,10,200,200, QtCore.Qt.AlignCenter, self.myText)
I have a QListView, where I display items using a custom delegate with custom painting. Within each item (i.e. each list row) I want to be able to show a couple of "hyperlinks" which the user could click on and which would then call on some functions.
I have already tried to check the official documentation (e.g. Model/View Programming) as well as quite a lot of googling, but haven't been able to figure out how to accomplish this.
I have two ideas, each with their own problems:
I could draw them using child widgets, like a flat QPushButton. How do I then position and display these widgets?
I could also draw them as text strings. How do I then make them clickable? Or can I capture click events on the parent QListView and somehow determine coordinates from those? I could then match coordinates to these clickable elements and act accordingly.
My initial approach was to use QListWidget with .setItemWidget(), where I had a proper widget with a layout and child widgets. Unfortunately this was too slow when my list grew to hundreds or thousands of items. That's why I changed to QListView with a delegate.
I seem to be closing in on a solution.
I can receive clicks on the elements by overriding the delegate's .editorEvent(event, model, option, index). I can then find out the event.type(), the clicked row from index.row() and the actual coordinates from event.x() and event.y() (since, if the event type is MouseButtonRelease, the event is a QMouseEvent).
From these, I think I can correlate the coordinates to my elements on screen and act accordingly.
I will update this answer once I have working code.
EDIT
A simple working example, using PySide:
class MyModel(QtGui.QStandardItemModel):
def __init__(self):
super(MyModel, self).__init__()
for i in range(10): self.appendRow(QtGui.QStandardItem("Row %d" % i))
class MyDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
super(MyDelegate, self).__init__(parent)
self.links = {}
def makeLinkFunc(self, row, text):
def linkFunc(): print("Clicked on %s in row %d" % (text, row))
return linkFunc
def paint(self, painter, option, index):
painter.save()
textHeight = QtGui.QFontMetrics(painter.font()).height()
painter.drawText(option.rect.x()+2, option.rect.y()+2+textHeight, index.data())
rowLinks = {}
for i in range(3):
text = "Link %d" % (3-i)
linkWidth = QtGui.QFontMetrics(font).width(text)
x = option.rect.right() - (i+1) * (linkWidth + 10)
painter.drawText(x, y, text)
rect = QtCore.QRect(x, y - textHeight, linkWidth, textHeight)
rowLinks[rect] = self.makeLinkFunc(index.row(), text)
self.links[index.row()] = rowLinks
painter.restore()
def sizeHint(self, option, index):
hint = super().sizeHint(option, index)
hint.setHeight(30)
return hint
def editorEvent(self, event, model, option, index):
if event.type() == QtCore.QEvent.MouseButtonRelease:
for rect, link in self.links[index.row()].items():
if rect.contains(event.pos()):
link()
return True
return False
listmodel = MyModel()
listview = QtGui.QListView()
listview.setModel(listmodel)
listview.setItemDelegate(MyDelegate(parent=listview))
listview.setSelectionMode(QtGui.QAbstractItemView.NoSelection)