LinearRegionItem add new contextMenu - pyqtgraph

For some days now, I am trying to generate a context menu for a LinearRegionItem (to be able to annotate the marked regions). In fact, I successfully created a new class AnnotationItem to overwrite several methods from LinearRegionItem. With contextMenuEvent, I have the possibility to trigger the generation of the context menu, however I am facing to problems:
1.) How do I prevent the default plot menu to be called when using the (in my case) right mouse button
(see default plot menu) without overall deactivating the menu? In the current implementation, the contextMenuEvent method is called, however, the default plot menu appears afterwards.
2.) How do I prevent the new menu from disappearing directly after it is called? See here for gif.
Thank you so much for your support.
Cheers,
Nico
class AnnotationItem(LinearRegionItem):
def __init__(self, *args, **kwargs):
super(AnnotationItem, self).__init__(*args, **kwargs, showSize=True)
def contextMenuEvent(self, event):
print('Method called...')
cmenu = QtWidgets.QMenu()
test1Act = cmenu.addAction("Test1")
test2Act = cmenu.addAction("Test2")
pos = event.screenPos()
cmenu.popup(QtCore.QPoint(pos.x(), pos.y()))

New week, found the solution:
add a cmenu.exec() at the end of the contextMenuEvent method and all problems are solved.
Thanks anyway...

Related

Refresh view when model data has not changed (Qt/PySide/PyQt)?

I have a tree view of a standard item model in which I can use a spinbox to change the row height in the view (see SSCCE below). This doesn't change the content of the view, only its appearance, sort of like resizing the main window except I have to do it myself:
I change the row height from within the delegate's sizeHint method. It is within sizeHint that I get the value from the spinbox and set the row height to that value. To make sure the size hint is actually called, I refresh the view when the spinbox value is changed.
My question is this: in such cases of purely cosmetic changes, what is the recommended way to tell the view to refresh? Is there some method built specifically for such cases?
Obviously, this question assumes my general strategy for adjusting row height is sound, which I am also open to correction on.
There are a few methods for telling the view that it is time to refetch the data and redraw things: layoutChanged, reset, setModel, dataChanged. Hell, I found that even just calling expandAll on the tree was enough to update my view to show the new row height.
In practice, I found using layoutChanged works:
QtGui.QStandardItemModel.layoutChanged.emit()
It is sort of uncommon usage, as that is more for when you have rearranged your data (e.g., by sorting). This is what I include in the SSCCE below, because it works. I also tried following the more commonly suggested practice of emitting dataChanged:
QtGui.QStandardItemModel.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
This does not work for me. Even if it did, it would also be something of a hack, because it is telling the view that the data has changed in the model. When it hasn't.
At any rate, there is a lot of discussion online about what to do when you change the data in your model (see Relevant Posts), but none I have found about what to do when you just want to simply refresh the view for purely cosmetic reasons.
Cross post
I posted the same question at Qt Centre:
http://www.qtcentre.org/threads/63982-Best-way-to-refresh-view-for-cosmetic-%28non-model%29-changes
I got an answer there that have incorporated into the accepted answer below.
Relevant posts
Qt Model-View update view?
PyQt - Automatically refresh a custom view when the model is updated?
http://www.qtcentre.org/threads/48230-QTreeView-How-to-refresh-the-view
http://www.qtcentre.org/threads/3145-QTableView-How-to-refresh-the-view-after-an-update-of-the-model
SSCCE
import sys
from PySide import QtGui, QtCore
class MainTree(QtGui.QMainWindow):
def __init__(self, parent = None):
QtGui.QMainWindow.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.createRowHeightSpinbox() #create first otherwise get errors
self.tree = SimpleTree(self)
self.setCentralWidget(self.tree)
#Add spinbox to toolbar
self.rowHeightAction = QtGui.QAction("Change row height", self)
self.toolbar = self.addToolBar("rowHeight")
self.toolbar.addWidget(QtGui.QLabel("Row height "))
self.toolbar.addWidget(self.rowHeightSpinBox)
#Expand and resize tree
self.tree.expandAll()
self.tree.resizeColumnToContents(0)
self.tree.resizeColumnToContents(1)
def createRowHeightSpinbox(self):
self.rowHeightSpinBox = QtGui.QSpinBox()
self.rowHeightSpinBox.setRange(10, 50)
self.rowHeightSpinBox.setValue(18)
self.rowHeightSpinBox.valueChanged.connect(self.refreshView) #showimage uses the spinbox attribute to scale image
def refreshView(self):
self.tree.model.layoutChanged.emit()
class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self, parent)
self.setUniformRowHeights(True) #optimize
self.model = QtGui.QStandardItemModel()
self.rootItem = self.model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('You have been blocked up')]
item00 = [QtGui.QStandardItem('Tickle nose'), QtGui.QStandardItem('Key first step')]
item1 = [QtGui.QStandardItem('Get a job'), QtGui.QStandardItem('Do not blow it')]
self.rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.rootItem.appendRow(item1)
self.setModel(self.model)
self.setItemDelegate(ExpandableRows(self))
class ExpandableRows(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
def sizeHint(self, option, index):
rowHeight = self.parent.window().rowHeightSpinBox.value()
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
document.setPlainText(text) #for html use setHtml
return QtCore.QSize(document.idealWidth() + 5, rowHeight)
def main():
app = QtGui.QApplication(sys.argv)
#myTree = SimpleTree()
myMainTree = MainTree()
myMainTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Solution
The principled solution is to emit QAbstractItemDelegate.sizeHintChanged when the spinbox value changes. This is because you only want to call sizeHint of your delegate, and that's exactly what this method does.
In the example in the OP, the size hint is intended to change when the value in the spinbox is changed. You can connect the valueChanged signal from the spinbox to the delegate's sizeHintChanged signal as follows:
class ExpandableRows(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
self.parent.window().rowHeightSpinBox.valueChanged.connect(self.emitSizeChange)
def emitSizeChange(self):
self.sizeHintChanged.emit(QtCore.QModelIndex())
Analysis
As indicated in the OP, you don't want to call dataChanged because it doesn't actually work, and because your data hasn't actually changed. Further, while calling layoutChanged works, it is less principled because it is technically meant to be used to tell the view that the model's items have been rearranged, which they have not.
Caveat: I believe that sizeHintChanged expects to use a valid index, but my solution is working with the invalid index. Because it works, I'm leaving it with the invalid QtCore.QModelIndex(). Perhaps someone can find an improvement on that and edit this answer.
Is principled better than fast?
Note when you do it the principled way, it actually is a little bit slower than using the layoutChanged trick. Specifically running layoutChanged takes about 70 microseconds, while emitting sizeHintChanged takes about 100 microseconds. This didn't depend on the size of the models I tested (up to 1000 rows). This difference of 30 microseconds is so small as to be negligible in most applications, but if someone really wants to fully optimize for speed, they might go with the layoutChanged trick.
The layoutChanged trick also has the benefit of being simpler: it doesn't involve messing around with the delegate, but uses intuitively simple methods on the main window class. Also because it doesn't depend on any methods being implemented in the delegate, the trick (arguably) seems to be more modular. The "proper" way depends on creating a more brittle dependence between the delegate and the main window, which means it will be easier to break the application when developers modify one or the other.
In sum, a case could be made that in just about every measurable way, the hack from the OP is better than the "principled" solution here.
Acknowledgment
I got turned on to the existence of sizeHintChanged from answers at the same question that I cross-posted at QtCentre:
http://www.qtcentre.org/threads/63982-Best-way-to-refresh-view-for-cosmetic-%28non-model%29-changes

QMimeData: setting and getting the right MIME types for arbitrary widgets

Using PySide, I construct a draggable label that works exactly how I want:
class DraggableLabel(QtGui.QLabel):
def __init__(self, txt, parent):
QtGui.QLabel.__init__(self, txt, parent)
self.setStyleSheet("QLabel { background-color: rgb(255, 255, 0)}")
def mouseMoveEvent(self, event):
drag=QtGui.QDrag(self)
dragMimeData=QtCore.QMimeData()
drag.setMimeData(dragMimeData)
drag.exec_(QtCore.Qt.MoveAction)
(Note a full example that uses DraggableLabel is pasted below). Unfortunately, I do not understand what is going on with QMimeData, and fear I am going to run into big problems when I use similar code in real-world examples.
In particular, I am worried that my reimplementation of mouseMoveEvent creates an instance of QMimeData without any argument passed: QtCore.QMimeData(). Is this normal? Within more complex widgets will I be OK if I keep doing that within the relevant event handler: will the program automatically create the right type of MIME data for dragging and dropping?
The reason I fear I am missing something is because at the Qt Drag and Drop documentation, it has lines of code like:
mimeData -> setText(commentEdit->toPlainText());
which seems decidedly not like just letting the program take care of things within a reimplementation of an event handler.
Also, the QMimeData Documentation discusses convenience functions to test, get, and set data, but those are for standard data types (e.g., text, urls). I have found no clear way to define such convenience functions for widgets like my draggable QLabel. Am I missing it? Is there a simple way to find out if I am dragging around a widget of type X?
Edit: I've tried the same code above with much more complicated widgets than QLabels, and it does not work.
Potentially relevant posts:
Dragging a QWidget in QT 5
How to Drag and Drop Custom Widgets?
https://stackoverflow.com/questions/18272650/fill-the-system-clipboard-with-data-of-custom-mime-type
Python object in QMimeData
Important Caveat: if you just want to move a widget in a window, you do not need to invoke esoteric drag-drop mechanisms, but more vanilla event handling. See this: Dragging/Moving a QPushButton in PyQt.
Full working self-contained code example that incorporates the above:
# -*- coding: utf-8 -*-
from PySide import QtGui, QtCore
class LabelDrag(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.initUI()
def initUI(self):
self.lbl=DraggableLabel("Drag me", self)
self.setAcceptDrops(True)
self.setGeometry(40,50,200,200)
self.show()
def dragEnterEvent(self,event):
event.accept()
def dropEvent(self, event):
self.lbl.move(event.pos()) #moves label to position once the movement finishes (dropped)
event.accept()
class DraggableLabel(QtGui.QLabel):
def __init__(self, txt, parent):
QtGui.QLabel.__init__(self, txt, parent)
self.setStyleSheet("QLabel { background-color: rgb(255, 255, 0)}")
def mouseMoveEvent(self, event):
drag=QtGui.QDrag(self)
dragMimeData=QtCore.QMimeData()
drag.setMimeData(dragMimeData)
drag.exec_(QtCore.Qt.MoveAction)
def main():
import sys
qt_app=QtGui.QApplication(sys.argv)
myMover=LabelDrag()
sys.exit(qt_app.exec_())
if __name__=="__main__":
main()
Note I'm posting this at QtCentre as well, will post anything useful from there.
In my experience, MimeData is used to filter drag/drop operations so that the action actually makes sense. For instance, you shouldn't be able to drag your QLabel into the middle of a QTextEdit or your browsers address bar, or the desktop of your computer.
From the docs:
QMimeData is used to describe information that can be stored in the clipboard, and transferred via the drag and drop mechanism. QMimeData objects associate the data that they hold with the corresponding MIME types to ensure that information can be safely transferred between applications, and copied around within the same application.
If you were doing something standard, like dragging/dropping text from one place to another, you would use one of the standard MIME types (like setting the MIME data of your drag using dragMimeData.setText('your text') or equivalently dragMimeData.setData('text/plain', 'your text')). However, since you are doing something completely custom, you should probably specify a custom MIME type so that you can't accidentally do things that don't make sense.
So I would set the MIME data to something like dragMimeData.setData('MoveQLabel', QByteArray('any string you want')) which stores an arbitrary string for the MIME type MoveQLabel. This arbitrary string could be used to look up which widget you want to move at the end of the drag (maybe by storing it's position?).
You should modify your LabelDrag class above to check the MIME type of the event (use event.mimeData() to get the QMimeData object you set when you started the drag), and accept or reject the event depending on whether the MIME type matches MoveQLabel (or whatever you call your custom MIME type). This should be done in both the dragEnterEvent and dropEvent methods.
You code would look something like:
def dragEnterEvent(self, event):
# you may also need to check that event.mimeData() returns a valid QMimeData object
if event.mimeData().hasFormat('MoveQLabel'):
event.accept()
else:
event.reject()
If you also store some useful MIME data with the MIME type (aka something else instead of 'any string you want' above), you could use it to dynamically select, within LabelDrag.dropEvent, which widget it is that you want to move. You can access it through event.mimeData().data('MoveQLabel'). This means that your QWidget can handle moving multiple labels as it will always move the one that is being dropped.

QWidget Geometry() not updating

I have QWidgets lined up in a QVBoxLayout inside a QScrollArea with setWidgetResizable(True)
When I resize one of the QWidgets, the QWidgets relocate themselves accordingly in the graphical user interface, but their geometry() property doesn't reflect that, geometry().x() and geometry().y() both remain the same before and after relocation.
The only method I have found so far to update the x() and y() coordinates is to hide() and show() the QScrollArea.
I have tried update(), updateGeometry() and repaint() without any success.
Here is a sample test code that I made to summarize the problem :
import sys
from PySide import QtGui, QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.showMaximized()
self.widget_window = QtGui.QWidget()
self.scrollarea = QtGui.QScrollArea()
self.v_layout = QtGui.QVBoxLayout()
self.test_1 = QtGui.QPushButton('Test 1')
self.test_2 = QtGui.QPushButton('Test 2')
self.v_layout.addWidget(self.test_1)
self.v_layout.addWidget(self.test_2)
self.widget_window.setLayout(self.v_layout)
self.scrollarea.setWidgetResizable(True)
self.scrollarea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.scrollarea.setAlignment(QtCore.Qt.AlignCenter)
self.scrollarea.setWidget(self.widget_window)
self.setCentralWidget(self.scrollarea)
print(self.test_2.geometry().x())
print(self.test_2.geometry().y())
self.test_1.setFixedHeight(1000)
#uncommenting the following next two lines, solves the problem
#self.scrollarea.hide()
#self.scrollarea.show()
print(self.test_2.geometry().x())
print(self.test_2.geometry().y())
def main():
app = QtGui.QApplication(sys.argv)
main = MainWindow()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
My questions are :
Is there a better solution ?
Isn't updateGeometry() supposed to actually update the geometry() ?
Why is this behavior happening ? (i.e why does it update graphically but not programmatically ?)
Can using hide() and show() successively cause some problems in other contexts (window flickering or something else...) ?
When you update the GUI, it's not instantaneous. It's performed when you give the control to the event loop. In this case, it's done after __init__ completes, which is obviously after all your prints. Therefore, you'll see the old position.
hide/show forces an update at that point. That's why you get correct values. However, this is not the best way, because you might observe flicker while hide/show does its job.
Better way would be telling the event loop to process events before continuing. This will ensure that the GUI is updated:
#...
print(self.test_2.geometry().x())
print(self.test_2.geometry().y())
self.test_1.setFixedHeight(1000)
QtGui.QApplication.processEvents()
print(self.test_2.geometry().x())
print(self.test_2.geometry().y())
#...
PS: I'm assuming, you have good reasons to do this. Most of the time, you won't care when the GUI is updated.

Detecting enter on a QLineEdit or QPushButton

I've built an app for a game, simple to start. It's a game in which the system randomly chooses a number and a gamer (player) tries to find out the number. Everything is almost done. The app consists of a QLineEdit, a label and three buttons. Once the app tells the player the range of the wanted number, he/she types a bet_number and clicks on the play button. And according to this number he/she gets a message about how close or far the wanted number is away from the bet_number.
But I find it a little disgusting to click a button. Instead I want to use Enter key to play. So to achieve this, it comes down to specifically two questions:
How could one change to using Enter to play (I mean I need know when QLineEdit detects enter key is pressed)? In this way I'll code properly to point the play method.
If the play button's got the focus, how do you use enter key on this button? (make Button accept Enter key)
For the QLineEdit connect to the returnPressed signal.
Alternatively, if you use the setAutoDefault method on your QPushButtons you emit the clicked signal when Enter is pressed on a focused QPushButton:
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import sip
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)
from PyQt4 import QtGui, QtCore
class MyWindow(QtGui.QWidget):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.pushButtonOK = QtGui.QPushButton(self)
self.pushButtonOK.setText("OK")
self.pushButtonOK.clicked.connect(self.on_pushButtonOK_clicked)
self.pushButtonOK.setAutoDefault(True)
self.lineEditNumber = QtGui.QLineEdit(self)
self.lineEditNumber.returnPressed.connect(self.pushButtonOK.click)
self.layoutHorizontal = QtGui.QHBoxLayout(self)
self.layoutHorizontal.addWidget(self.pushButtonOK)
self.layoutHorizontal.addWidget(self.lineEditNumber)
#QtCore.pyqtSlot()
def on_pushButtonOK_clicked(self):
inputNumber = self.lineEditNumber.text()
if inputNumber.isdigit():
info = "You selected `{0}`"
else:
info = "Please select a number, `{0}` isn't valid!"
print info.format(inputNumber)
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
app.setApplicationName('MyWindow')
main = MyWindow()
main.show()
sys.exit(app.exec_())
QLineEdit will emit the signal returnPressed() whenever the user presses the enter key while in it: http://qt-project.org/doc/qt-4.8/qlineedit.html#signals. You can either connect this signal to your button's click() slot or directly call whatever your button's clicked() signal was connected to.
A slight C++ variation on other answers, it's not significantly different, but I thought i would include it anyway because how you lay things in QT code can be very different from codebase to codebase and I wanted to strip out the extraneous stuff to give the shortest and easiest to understand excerpt of code.
QLineEdit *TextSend = new QLineEdit("");
QPushButton *SendPB = new QPushButton("Send!");
connect(TextSend, &QLineEdit::returnPressed, this, &CLITab::SendCommand);
connect(SendPB, &QPushButton::released, this, &CLITab::SendCommand);
So what this is doing is we create a QLineEdit textbox and a QPushbutton.
We do cosmetic things like set the string label for them and add them to our layout.
Then we setup a callback handler, which will be triggered when the QLineEdit returns "returnPressed", which then calls automatically into a function which i wrote called "CLITab::SendCommand()", and then it's upto this function to extract the data out of QLineEdit and do whatever needs to be done. In practice the TextSend and SendPB pointers would live in the parent class, so that SendCommand() has visibility over these objects.
Just putting this here, along side an example pushbutton, because essentially they work in precisely the same way, all that's different is the signal name emitted.

Set bold rows in a QTreeView

I have a custom subclass of a QTreeView in a pyqt application. I'm trying to give the user the ability to highlight and "lowlight" (for lack of a better term) rows. Highlighted rows should have bold text and (optionally) a different background color. Any ideas?
I'm considering StyleSheets as an option, but have so far been unable to get that to work. If I set the QTreeView's stylesheet:
self.setStyleSheet("QTreeView::item:selected {border: 1px solid #567dbc;}")
I can't figure out how to manually enable 'states' that would keep only the desired rows at a particular state. If I try setting an individual item's stylesheet:
#modelIndex is a valid QModelIndex
modelIndex.internalPointer().setStyleSheet()
I get a segfault.
I'm not convinced stylesheets are the way to go, I'm open to all ideas. Thanks!
From what you've said it looks like the easiest solution would be to define a custom item delegate for your treeview and set items font weight to bold whenever it's needed. Pls, check if an example below would work for you, it should create a treeview with custom item delegate which would change item's font style.
import sys
from PyQt4 import QtGui, QtCore
class BoldDelegate(QtGui.QStyledItemDelegate):
def paint(self, painter, option, index):
# decide here if item should be bold and set font weight to bold if needed
option.font.setWeight(QtGui.QFont.Bold)
QtGui.QStyledItemDelegate.paint(self, painter, option, index)
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
model = QtGui.QStandardItemModel()
for k in range(0, 4):
parentItem = model.invisibleRootItem()
for i in range(0, 4):
item = QtGui.QStandardItem(QtCore.QString("item %0 %1").arg(k).arg(i))
parentItem.appendRow(item)
parentItem = item
self.view = QtGui.QTreeView()
self.view.setModel(model)
self.view.setItemDelegate(BoldDelegate(self))
self.setCentralWidget(self.view)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
hope this helps, regards
I can think of a few ways to do this. The easiest, if you have access to the model, is to add some state tracking of the indexes in the model, and return the proper options for the roles requested in the data() function. The drawback to this is if you are using the same model in different views, and want to keep the highlights local to one view.
The second-easiest is probably to make a proxy model, which keeps track of the data itself, and gets all other data from the original model. In this situation (where you aren't changing the rows or columns of the original model) it would probably be pretty easy.
The hardest would be to make a custom delegate that keeps track of which rows/columns should be highlighted, and draws itself differently based on the row/column of the model index it is drawing. You would have to keep access to the delegate so that you could tell it which rows and columns need to be set. You would also need to deal with what happens when the model changes.

Resources