vtkOrientationMarkerWidget embedded in QTWidget - python

I have embedded the vtkOrientationMarkerWidget to the QTWidget as an axes indicator, but I met a problem there arises a bug:
ERROR: In C:\Dev\Soft\vtk\source\Rendering\OpenGL2\vtkWin32OpenGLRenderWindow.cxx, line 217
vtkWin32OpenGLRenderWindow (00000278F82D1AD0): wglMakeCurrent failed in MakeCurrent(), error: Handle
Invalid
when I multiply generate QTWidget instances by clicking the button.
Because I want to add some buttons in the 3D view, I use QTWidget as a container, which is the root cause of the problem. Are there some solutions for this?
My environment is Python 3.8+PyQT5.15.0+VTK9.0.1
The code is:
The main window:
from PyQt5.QtWidgets import QMainWindow, QDesktopWidget, QFrame, QVBoxLayout, QApplication, QPushButton
from axes_widget import VTKWidget2
from axes import Axes
from axes2 import Axes2
class Widget:
def __init__(self):
self.window = QMainWindow()
self.window.resize(500, 400)
self.window.move(300, 310)
self.button = QPushButton('3D', self.window)
self.button.clicked.connect(self.click_3d)
self.button.move(380, 80)
def click_3d(self):
self._win_vtk = VTKWidget2()
self._win_vtk.show()
if __name__ == '__main__':
app = QApplication([])
widget_ins=Widget()
widget_ins.window.show()
app.exec_()
The VTK window:
from PyQt5.QtWidgets import QMainWindow, QDesktopWidget, QFrame, QVBoxLayout, QApplication
import sys
import vtk
import vtkmodules.qt
vtkmodules.qt.QVTKRWIBase = "QGLWidget"
# QGLWidget as the base class of the QVTKRenderWindowInteractor, instead of QWidget.
# This change is because it is reported that sometimes QWidget can cause rendering problems.
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from serum import dependency, singleton, inject
# #singleton
class VTKWidget2(QMainWindow):
def __init__(self, parent=None):
super(VTKWidget2, self).__init__(parent)
self.camera_focal_point = [0, 0, 0]
self.camera_position = [-4500, 0, 0]
self.camera_view_up = [0, -1, 0] # default, [0,1,0]
self.camera_azimuth = 0 # -20
self.camera_zoom = 1.8 # 1.75
self.z_angle = 0
self.x_angle = 0
self.y_angle = 0
self.x_transform = 200
self.y_transform = -900 # -820
self.z_transform = 1000
# vtk.vtkOutputWindow.SetGlobalWarningDisplay(0)
self.initial()
def initial(self):
self.setWindowTitle("3D_Model")
self.resize(1000, 800)
screen = QDesktopWidget().geometry()
self.self_size = self.geometry()
self.move(int((screen.width() - self.self_size.width()) / 2),
int((screen.height() - self.self_size.height()) / 2)) #
self.colors = vtk.vtkNamedColors()
# Create an actor
# self.actor = vtk.vtkActor()
self.left_text_actor = vtk.vtkTextActor()
self.right_text_actor = vtk.vtkTextActor()
# A renderer and render window
self.renderer = vtk.vtkRenderer()
# renderWindow = vtk.vtkRenderWindow()
# renderWindow.SetWindowName("Display Coordinate Axes")
# renderWindow.AddRenderer(renderer)
# An interactor
# renderWindowInteractor = vtk.vtkRenderWindowInteractor()
# renderWindowInteractor.SetRenderWindow(renderWindow)
self.renderWindowInteractor = QVTKRenderWindowInteractor()
self.renderWindow = self.renderWindowInteractor.GetRenderWindow()
self.renderWindow.AddRenderer(self.renderer)
# Add the actors to the scene
# self.renderer.AddActor(self.actor)
self.renderer.SetBackground(self.colors.GetColor3d("SlateGray"))
# add mouse interaction mode
vtkStyle = vtk.vtkInteractorStyleTrackballCamera()
# vtkStyle = MyInteractorStyle(self.renderWindow)
# vtkStyle = vtk.vtkInteractorStyleSwitch()
# vtkStyle = vtk.vtkInteractorStyleTrackballActor()
self.renderWindowInteractor.SetInteractorStyle(vtkStyle)
# self.renderWindow.GetInteractor().SetInteractorStyle(vtkStyle)
self.transform = vtk.vtkTransform()
self.transform.Translate(self.x_transform, self.y_transform, self.z_transform)
# set axes
self.add_axes()
self.vtkCamera = vtk.vtkCamera()
self.update_camera()
frame = QFrame()
self.setCentralWidget(frame) # QMainWindow's property
vl = QVBoxLayout()
vl.addWidget(self.renderWindowInteractor)
# vl.addWidget(self.widget) # problem: wrong: no widget for QT
frame.setLayout(vl)
# Begin mouse interaction
self.renderWindowInteractor.Initialize()
self.renderWindowInteractor.Start()
def add_axes(self):
# set axes
### important: widget must be set as field, otherwise it doesn't show.
# problem: no widget for QT
self.widget = vtk.vtkOrientationMarkerWidget()
self.axis = vtk.vtkAxesActor()
rgba = [0] * 4
self.colors.GetColor("Carrot", rgba)
self.widget.SetOutlineColor(rgba[0], rgba[1], rgba[2])
self.widget.SetOrientationMarker(self.axis) ### important
self.widget.SetInteractor(self.renderWindowInteractor)
self.widget.SetViewport(0.0, 0.0, 0.4, 0.4)
self.widget.SetEnabled(1)
self.widget.InteractiveOn()
def update_camera(self):
self.renderer.ResetCamera()
self.renderer.SetActiveCamera(self.vtkCamera)
self.vtkCamera.SetFocalPoint(self.camera_focal_point)
self.vtkCamera.SetPosition(self.camera_position)
self.vtkCamera.SetViewUp(self.camera_view_up)
self.vtkCamera.Azimuth(self.camera_azimuth)
self.vtkCamera.Zoom(self.camera_zoom)
self.renderWindow.Render()

The problem lies in Widget.click_3d(self). Every time you click the button you reassign a new VTKWidget2 to self._win_vtk. Since self._win_vtk is the only reference in your program to any of the VTKWidget2 windows, as soon as you assign a new value to self._win_vtk, the previous VTKWidget2 window will be deleted by the garbage collector (which apparently causes problems with the underlying QGLWidget). One way around this is to make a persistent reference to all the windows, for example by putting them in a list, e.g.
class Widget:
def __init__(self):
....
self.vtk_windows = []
def click_3d(self):
win = VTKWidget2()
self.vtk_windows.append(win)
win.show()
This still causes a bunch of errors when the last window is closed an the program exits which seems to be cause by using a QGLWidget as the base of your QVTKRenderWindowInteractor.

Related

Print items to pdf

I have a window with a QGraphicsScene as painter, and i want to render its elements to a pdf file on press of a button.
def generateReport(self):
lineList = {}
for i in self.circleList:
for j,k in i.lineItems:
if j not in lineList:
lineList[j] = [i, k]
printed = QPdfWriter("Output.pdf")
printed.setPageSize(QPagedPaintDevice.A4)
printer = QPainter(printed)
self.painter.render(printer)
for i,j in enumerate(lineList):
# j.scene().render(printer)
# lineList[j][0].scene().render(printer)
# lineList[j][1].scene().render(printer)
printer.drawText(0, self.painter.height() + i*200, f'{j.nameItem.toPlainText()}: {lineList[j][0].m_items[4].toPlainText()}, {lineList[j][1].m_items[4].toPlainText()}')
printer.end()
nameItem on j is the name label for the line, m_items[4] is the name label for each circle.
My issue is that i cant seem to get the exact height of the rendered scene, moreover I have zero clue as to how i could overflow the text to the next page should the contents not fit in one.
it would be lovely if i could somehow render every line and its corresponding circles seperately for each connection, stored in lineList
note: the line is a child of every circle , and the names of every line and circle are children of theirs, implemented much in the same way as the answer to my previous question where in lies my final issue of the grip items also being rendered.
I have discovered that I can create a new scene, move each item one by one and render it out to the pdf but this raises two separate issues
I cant add a line break and avoid overdrawing the new render over the previous one, and
I cant position the text as addText doesnt take positional arguments.
MRE:
import random
from fbs_runtime.application_context.PyQt5 import ApplicationContext
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPdfWriter, QBrush, QPagedPaintDevice
from PyQt5.QtWidgets import (QDialog, QGraphicsScene,
QGraphicsView, QGridLayout,
QPushButton, QGraphicsEllipseItem)
class gui(QDialog):
def __init__(self, parent=None):
super(gui, self).__init__(parent)
self.resize(1280, 720)
self.painter = QGraphicsScene(0, 0, self.width() - 50, self.height() - 70)
self.painter.setBackgroundBrush(QBrush(Qt.white))
self.canvas = QGraphicsView(self.painter)
mainLayout = QGridLayout()
mainLayout.addWidget(self.canvas, 0, 0, -1, -1)
self.setLayout(mainLayout)
#property
def circleList(self):
return [item for item in self.painter.items() if isinstance(item, QGraphicsEllipseItem)]
def newCircle(self):
self.painter.addEllipse( random.randint(100, 400), random.randint(100, 400), 50 + random.random() * 200, 50 + random.random() * 200)
def generateReport(self):
printed = QPdfWriter("Output.pdf")
printed.setPageSize(QPagedPaintDevice.A4)
printer = QPainter(printed)
self.painter.render(printer)
for i,j in enumerate(self.circleList):
printer.drawText(0, printer.viewport().height() + i*200, 'test')
printer.end()
if __name__ == "__main__":
app = ApplicationContext()
test = gui()
test.newCircle()
test.newCircle()
test.newCircle()
test.generateReport()
test.show()
exit(app.app.exec_())
if possible , the ability to print, test then circle for all circles would be decent enough for me.
Incorrect output example:
To understand what painting is like, you have to understand how QGraphicsScene::render() method works:
void QGraphicsScene::render(QPainter *painter, const QRectF &target = QRectF(), const QRectF &source = QRectF(), Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio)
Renders the source rect from scene into target, using painter. This
function is useful for capturing the contents of the scene onto a
paint device, such as a QImage (e.g., to take a screenshot), or for
printing with QPrinter. For example:
QGraphicsScene scene;
scene.addItem(...
...
QPrinter printer(QPrinter::HighResolution);
printer.setPaperSize(QPrinter::A4);
QPainter painter(&printer);
scene.render(&painter);
If source is a null rect, this function will use sceneRect() to
determine what to render. If target is a null rect, the dimensions of
painter's paint device will be used.
The source rect contents will be transformed according to
aspectRatioMode to fit into the target rect. By default, the aspect
ratio is kept, and source is scaled to fit in target.
See also QGraphicsView::render().
In your case, if the source is not passed, the entire sceneRect (0, 0, 1230, 650) will be copied and painted on the pdf page, if the sizes do not match, the sizes will be scaled. So from the above it follows that if you want to print an item then you must pass as source the space it occupies in the scene and hide the other items, and the target is the place where you want to paint, which involves calculating the new position based on where the previous item was printed.
Considering the above, a possible solution is the following:
import random
from PyQt5 import QtCore, QtGui, QtWidgets
class Gui(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Gui, self).__init__(parent)
self.resize(1280, 720)
self.scene = QtWidgets.QGraphicsScene(
0, 0, self.width() - 50, self.height() - 70
)
self.scene.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.white))
self.canvas = QtWidgets.QGraphicsView(self.scene)
mainLayout = QtWidgets.QGridLayout(self)
mainLayout.addWidget(self.canvas)
#property
def circleList(self):
return [
item
for item in self.scene.items()
if isinstance(item, QtWidgets.QGraphicsEllipseItem)
]
def newCircle(self):
self.scene.addEllipse(
random.randint(100, 400),
random.randint(100, 400),
50 + random.random() * 200,
50 + random.random() * 200,
)
def generateReport(self):
printer = QtGui.QPdfWriter("Output.pdf")
printer.setPageSize(QtGui.QPagedPaintDevice.A4)
printer.setResolution(100)
painter = QtGui.QPainter(printer)
delta = 20
f = painter.font()
f.setPixelSize(delta)
painter.setFont(f)
# hide all items
last_states = []
for item in self.scene.items():
last_states.append(item.isVisible())
item.setVisible(False)
target = QtCore.QRectF(0, 0, printer.width(), 0)
for i, item in enumerate(self.circleList):
item.setVisible(True)
source = item.mapToScene(item.boundingRect()).boundingRect()
target.setHeight(source.height())
if target.bottom() > printer.height():
printer.newPage()
target.moveTop(0)
self.scene.render(painter, target, source)
f = painter.font()
f.setPixelSize(delta)
painter.drawText(
QtCore.QRectF(
target.bottomLeft(), QtCore.QSizeF(printer.width(), delta + 5)
),
"test",
)
item.setVisible(False)
target.setTop(target.bottom() + delta + 20)
# restore visibility
for item, state in zip(self.scene.items(), last_states):
item.setVisible(state)
painter.end()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Gui()
for _ in range(200):
w.newCircle()
w.generateReport()
w.show()
sys.exit(app.exec_())

QSplitter, QWidget resizing, setSizes(), setStretchFactor(), and sizeHint() - how to make it all work together?

I'm struggling with working out how to make all the stuff in the title work together in a certain situation. I'm using PyQt5 here, but feel free to respond with regular C++ Qt as I can translate pretty easily.
I'm attempting to make a UI with the following:
A main form (inherits from QWidget, could just as well use QMainWindow)
The main form should contain a QSplitter oriented vertically containing a QTextEdit at the top and containing a custom class (inheriting from QLabel) to show an image taking up the rest of the space.
The QTextEdit at the top should default to about 3 lines of text high, but this should be resizable to any reasonable extreme via the QSplitter.
The custom class should resize the image to be as big as possible given the available space while maintaining the aspect ratio.
Of course the tricky part is getting everything to resize correctly depending on how big a monitor the user has and how the move the form around. I need this to run on screens as small as about 1,000 px width and perhaps as big as 3,000+ px width.
Here is what I have so far:
# QSplitter3.py
import cv2
import numpy as np
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QSizePolicy, \
QFrame, QTabWidget, QTextEdit, QSplitter
from PyQt5.QtGui import QImage, QPixmap, QPainter
from PyQt5.Qt import Qt
from PyQt5.Qt import QPoint
def main():
app = QApplication([])
screenSize = app.primaryScreen().size()
print('screenSize = ' + str(screenSize.width()) + ', ' + str(screenSize.height()))
mainForm = MainForm(screenSize)
mainForm.show()
app.exec()
# end function
class MainForm(QWidget):
def __init__(self, screenSize):
super().__init__()
# set the title and size of the Qt QWidget window
self.setWindowTitle('Qt Window')
self.setGeometry(screenSize.width() * 0.2, screenSize.height() * 0.2,
screenSize.width() * 0.5 , screenSize.height() * 0.7)
# declare a QTextEdit to show user messages at the top, set the font size, height, and read only property
self.txtUserMessages = QTextEdit()
self.setFontSize(self.txtUserMessages, 14)
self.txtUserMessages.setReadOnly(True)
# make the min height of the text box about 2 lines of text high
self.txtUserMessages.setMinimumHeight(self.getTextEditHeightForNLines(self.txtUserMessages, 2))
# populate the user messages text box with some example text
self.txtUserMessages.append('message 1')
self.txtUserMessages.append('message 2')
self.txtUserMessages.append('message 3')
self.txtUserMessages.append('stuff here')
self.txtUserMessages.append('bla bla bla')
self.txtUserMessages.append('asdasdsadds')
# instantiate the custom ImageWidget class below to show the image
self.imageWidget = ImageWidget()
self.imageWidget.setMargin(0)
self.imageWidget.setContentsMargins(0, 0, 0, 0)
self.imageWidget.setScaledContents(True)
self.imageWidget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.imageWidget.setAlignment(Qt.AlignCenter)
# declare the splitter, then add the user message text box and tab widget
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.txtUserMessages)
self.splitter.addWidget(self.imageWidget)
defaultTextEditHeight = self.getTextEditHeightForNLines(self.txtUserMessages, 3)
print('defaultTextEditHeight = ' + str(defaultTextEditHeight))
# How can I use defaultTextEditHeight height here, but still allow resizing ??
# I really don't like this line, the 1000 is a guess and check that may only work with one screen size !!!
self.splitter.setSizes([defaultTextEditHeight, 1000])
# Should setStretchFactor be used here ?? This does not seem to work
# self.splitter.setStretchFactor(0, 0)
# self.splitter.setStretchFactor(1, 1)
# What about sizeHint() ?? Should that be used here, and if so, how ??
# set the main form's layout to the QGridLayout
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.splitter)
self.setLayout(self.gridLayout)
# open the two images in OpenCV format
self.openCvImage = cv2.imread('image.jpg')
if self.openCvImage is None:
print('error opening image')
return
# end if
# convert the OpenCV image to QImage
self.qtImage = openCvImageToQImage(self.openCvImage)
# show the QImage on the ImageWidget
self.imageWidget.setPixmap(QPixmap.fromImage(self.qtImage))
# end function
def setFontSize(self, widget, fontSize):
font = widget.font()
font.setPointSize(fontSize)
widget.setFont(font)
# end function
def getTextEditHeightForNLines(self, textEdit, numLines):
fontMetrics = textEdit.fontMetrics()
rowHeight = fontMetrics.lineSpacing()
rowHeight = rowHeight * 1.21
textEditHeight = int(numLines * rowHeight)
return textEditHeight
# end function
# end class
def openCvImageToQImage(openCvImage):
# get the height, width, and num channels of the OpenCV image, then compute the byte value
height, width, numChannels = openCvImage.shape
byteValue = numChannels * width
# make the QImage from the OpenCV image
qtImage = QImage(openCvImage.data, width, height, byteValue, QImage.Format_RGB888).rgbSwapped()
return qtImage
# end function
class ImageWidget(QLabel):
def __init__(self):
super(QLabel, self).__init__()
# end function
def setPixmap(self, pixmap):
self.pixmap = pixmap
# end function
def paintEvent(self, event):
size = self.size()
painter = QPainter(self)
point = QPoint(0, 0)
scaledPixmap = self.pixmap.scaled(size, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
point.setX((size.width() - scaledPixmap.width()) / 2)
point.setY((size.height() - scaledPixmap.height()) / 2)
painter.drawPixmap(point, scaledPixmap)
# end function
# end class
if __name__ == '__main__':
main()
Currently I'm testing on a 2560x1440 screen and with the magic 1000 entered it works on this screen size, but I really don't like the hard-coded 1000. I suspect the area of the code where I'm missing something is this part:
# declare the splitter, then add the user message text box and tab widget
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.txtUserMessages)
self.splitter.addWidget(self.imageWidget)
defaultTextEditHeight = self.getTextEditHeightForNLines(self.txtUserMessages, 3)
print('defaultTextEditHeight = ' + str(defaultTextEditHeight))
# How can I use defaultTextEditHeight height here, but still allow resizing ??
# I really don't like this line, the 1000 is a guess and check that may only work with one screen size !!!
self.splitter.setSizes([defaultTextEditHeight, 1000])
# Should setStretchFactor be used here ?? This does not seem to work
# self.splitter.setStretchFactor(0, 0)
# self.splitter.setStretchFactor(1, 1)
# What about sizeHint() ?? Should that be used here, and if so, how ??
# set the main form's layout to the QGridLayout
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.splitter)
With the hard coded 1000 and on this particular screen it works pretty well:
To reiterate (hopefully more clearly) I'm attempting to be able to remove the hard-coded 1000 and command Qt as follows:
Initially make the form take up about 2/3 of the screen
Initially make the text box about 3 lines of text high (min of 2 lines of text high)
Allow the user to use the QSplitter to resize the text box and image at any time and without limit
When the form is resized (or minimized or maximized), resize the text box and image proportionally per how the user had them at the time of the resize
I've tried about every combination of the stuff mentioned in the title and so far in this post but I've not been able to get this functionality, except with the hard-coded 1000 that probably won't work with a different screen size.
How can I remove the hard-coded 1000 and modify the above to achieve the intended functionality?
In my solution I will not take into account the part of opencv since it adds unnecessary complexity.
The solution is to use the setStretchFactor() method, in this case override the sizeHint() method of the QTextEdit to set the initial size and setMinimumHeight() for the minimum height. To show the image I use a QGraphicsView instead of the QLabel since the logic is easier.
from PyQt5 import QtCore, QtGui, QtWidgets
class TextEdit(QtWidgets.QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
font = self.font()
font.setPointSize(14)
self.setFont(font)
self.setMinimumHeight(self.heightForLines(2))
def heightForLines(self, n):
return (
n * self.fontMetrics().lineSpacing() + 2 * self.document().documentMargin()
)
def showEvent(self, event):
self.verticalScrollBar().setValue(self.verticalScrollBar().minimum())
def sizeHint(self):
s = super().sizeHint()
s.setHeight(self.heightForLines(3))
return s
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setBackgroundBrush(self.palette().brush(QtGui.QPalette.Window))
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = QtWidgets.QGraphicsPixmapItem()
scene.addItem(self._pixmap_item)
def setPixmap(self, pixmap):
self._pixmap_item.setPixmap(pixmap)
def resizeEvent(self, event):
self.fitInView(self._pixmap_item, QtCore.Qt.KeepAspectRatio)
self.centerOn(self._pixmap_item)
super().resizeEvent(event)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.textedit = TextEdit()
for i in range(10):
self.textedit.append("Message {}".format(i))
self.graphicsview = GraphicsView()
self.graphicsview.setPixmap(QtGui.QPixmap("image.jpg"))
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
splitter.addWidget(self.textedit)
splitter.addWidget(self.graphicsview)
splitter.setStretchFactor(1, 1)
lay = QtWidgets.QGridLayout(self)
lay.addWidget(splitter)
screenSize = QtWidgets.QApplication.primaryScreen().size()
self.setGeometry(
screenSize.width() * 0.2,
screenSize.height() * 0.2,
screenSize.width() * 0.5,
screenSize.height() * 0.7,
)
def main():
app = QtWidgets.QApplication([])
w = Widget()
w.resize(640, 480)
w.show()
app.exec_()
if __name__ == "__main__":
main()

How to avoid Mayavi pipeline pollution?

Below is some minimal code that fully demonstrates what I call "pipeline pollution". Each time you press the 'Draw' button, the MayaviScene editor (accessed via the top-left button on the figure) will update the figure, but also create a new scene's "shell" that lingers in the pipeline (as seen in the attached image).
I'm worried that in my more complex project, this pileup will have adverse effects.
Can someone please guide me on how to best set up this Mayavi scene to simply be updated without excess accumulation? I've read through tons of online materials, but still don't understand the developer's logic.
import sys, os
import numpy as np
from pyface.qt import QtGui, QtCore
os.environ['ETS_TOOLKIT'] = 'qt4'
from traits.api import HasTraits,Instance,on_trait_change
from traitsui.api import View,Item
from mayavi import mlab
from mayavi.core.ui.api import MayaviScene, MlabSceneModel, SceneEditor
class Mayavi_Scene(HasTraits):
scene = Instance(MlabSceneModel, ())
def update_scene(self):
Mayavi_Scene.fig1 = mlab.figure(1, bgcolor=(.5,.5,.5))
self.scene.mlab.clf(figure=Mayavi_Scene.fig1)
splot = mlab.points3d(P1.x, P1.y, P1.z,
scale_factor=0.05, figure=Mayavi_Scene.fig1)
view = View(Item('scene', editor = SceneEditor(scene_class=MayaviScene),
height=300, width=300, show_label=False),
resizable=True,
)
class P1(QtGui.QWidget):
# data starts out empty, wait for user input (below, via 'draw()'):
x = []
y = []
z = []
def __init__(self, parent=None):
super(P1, self).__init__(parent)
layout = QtGui.QGridLayout(self)
layout.setContentsMargins(20,20,20,20)
layout.setSpacing(10)
self.viz1 = Mayavi_Scene()
self.ui1 = self.viz1.edit_traits(parent=self, kind='subpanel').control
layout.addWidget(self.ui1, 0, 0, 1, 1)
def draw(): #a sample user input, could have been a custom data file, etc.
P1.x = np.random.random((100,))
P1.y = np.random.random((100,))
P1.z = np.random.random((100,))
Mayavi_Scene().update_scene()
#repeated presses pollute MayaviScene pipeline
# button to draw data:
self.btn1 = QtGui.QPushButton('Draw',self)
self.connect(self.btn1, QtCore.SIGNAL('clicked()'), draw)
layout.addWidget(self.btn1, 1, 0, 1, 1)
self.btn1.show()
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.window = P1(self)
self.setCentralWidget(self.window)
self.show()
if __name__ == '__main__':
app = QtGui.QApplication.instance()
w = MainWindow()
sys.exit(app.exec_())
The cause is likely the line containing Mayavi_Scene().update_scene() in the draw internal function. Every time draw is called, it creates a new Mayavi_Scene. The following P1 class instead defines draw as a method that accesses self.viz1 directly. I've also replaced the reference to draw with a reference to self.draw
class P1(QtGui.QWidget):
# data starts out empty, wait for user input (below, via 'draw()'):
x = []
y = []
z = []
def __init__(self, parent=None):
super(P1, self).__init__(parent)
layout = QtGui.QGridLayout(self)
layout.setContentsMargins(20,20,20,20)
layout.setSpacing(10)
self.viz1 = Mayavi_Scene()
self.ui1 = self.viz1.edit_traits(parent=self, kind='subpanel').control
layout.addWidget(self.ui1, 0, 0, 1, 1)
# button to draw data:
self.btn1 = QtGui.QPushButton('Draw',self)
# Connect the widget's draw method and the button
self.connect(self.btn1, QtCore.SIGNAL('clicked()'), self.draw)
layout.addWidget(self.btn1, 1, 0, 1, 1)
self.btn1.show()
def draw(self): #a sample user input, could have been a custom data file, etc.
P1.x = np.random.random((100,))
P1.y = np.random.random((100,))
P1.z = np.random.random((100,))
# Update the current scene without creating a new one.
self.viz1.update_scene()

Mayavi: Customize toolbar

Is there a way to customize the default toolbar of a mayavi scene? I would like to delete some buttons as I don't need them (e.g. the save button). Here you can see which toolbar I am talking about:
The code is just an example code:
import os
os.environ['ETS_TOOLKIT'] = 'qt4'
from pyface.qt import QtGui, QtCore
from traits.api import HasTraits, Instance, on_trait_change
from traitsui.api import View, Item
from mayavi.core.ui.api import MayaviScene, MlabSceneModel, SceneEditor
from tvtk.pyface.api import DecoratedScene
from pyface.api import ImageResource
from pyface.action.api import Action
class MyCustomScene(DecoratedScene):
def _actions_default(self):
actions = [
Action(
image = ImageResource("path to image",
search_path = [self._get_image_path()],
),
tooltip = "blabla",
on_perform = self._save_snapshot,
)
]
actions.extend(DecoratedScene._actions_default(self))
return actions
#The actual visualization
class Visualization(HasTraits):
scene = Instance(MlabSceneModel, ())
#on_trait_change('scene.activated')
def update_plot(self):
# We can do normal mlab calls on the embedded scene.
self.scene.mlab.test_points3d()
# the layout of the dialog screated
view = View(Item('scene', editor=SceneEditor(scene_class=MyCustomScene),
height=250, width=300, show_label=False),
resizable=True # We need this to resize with the parent widget
)
################################################################################
# The QWidget containing the visualization, this is pure PyQt4 code.
class MayaviQWidget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
layout = QtGui.QVBoxLayout(self)
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
self.visualization = Visualization()
# The edit_traits call will generate the widget to embed.
self.ui = self.visualization.edit_traits(parent=self,
kind='subpanel').control
layout.addWidget(self.ui)
self.ui.setParent(self)
if __name__ == "__main__":
# Don't create a new QApplication, it would unhook the Events
# set by Traits on the existing QApplication. Simply use the
# '.instance()' method to retrieve the existing one.
app = QtGui.QApplication.instance()
container = QtGui.QWidget()
container.setWindowTitle("Embedding Mayavi in a PyQt4 Application")
# define a "complex" layout to test the behaviour
layout = QtGui.QGridLayout(container)
# put some stuff around mayavi
label_list = []
for i in range(3):
for j in range(3):
if (i==1) and (j==1):continue
label = QtGui.QLabel(container)
label.setText("Your QWidget at (%d, %d)" % (i,j))
label.setAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter)
layout.addWidget(label, i, j)
label_list.append(label)
mayavi_widget = MayaviQWidget(container)
layout.addWidget(mayavi_widget, 1, 1)
container.show()
window = QtGui.QMainWindow()
window.setCentralWidget(container)
window.show()
# Start the main event loop.
app.exec_()
I think it is somewhere hidden in the MayaviScene. Maybe it is necessary to create a new class with a new scene or something like this?
You should check _actions_default for MayaviScene and for DecoratedScene to see how to create one of your own. The second one shows how to create a toolbar from scratch while the first one shows how your toolbar code interfaces with other components.
class MyCustomScene(DecoratedScene):
# …
def _actions_default(self):
actions = [
# add icons here
# …
]
return actions

mapToScene requires the view being shown for correct transformations?

Primary issue: the QGraphicsView.mapToScene method returns different answers depending on whether or not the GUI is shown. Why, and can I get around it?
The context is I'm trying to write unit tests but I don't want to actually show the tools for the tests.
The small example below illustrates the behavior. I use a sub-classed view that prints mouse click event positions in scene coordinates with the origin at the lower left (it has a -1 scale vertically) by calling mapToScene. However, mapToScene does not return what I am expecting before the dialog is shown. If I run the main section at the bottom, I get the following output:
Size is (150, 200)
Putting in (50, 125) - This point should return (50.0, 75.0)
Before show(): PyQt5.QtCore.QPointF(84.0, -20.0)
After show() : PyQt5.QtCore.QPointF(50.0, 75.0)
Before show(), there is a consistent offset of 34 pixels in x and 105 in y (and in y the offset moves in reverse as if the scale is not being applied). Those offset seem rather random, I have no idea where they are coming from.
Here is the example code:
import numpy as np
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPointF, QPoint
from PyQt5.QtWidgets import (QDialog, QGraphicsView, QGraphicsScene,
QVBoxLayout, QPushButton, QApplication,
QSizePolicy)
from PyQt5.QtGui import QPixmap, QImage
class MyView(QGraphicsView):
"""View subclass that emits mouse events in the scene coordinates."""
mousedown = pyqtSignal(QPointF)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSizePolicy(QSizePolicy.Fixed,
QSizePolicy.Fixed)
# This is the key thing I need
self.scale(1, -1)
def mousePressEvent(self, event):
return self.mousedown.emit(self.mapToScene(event.pos()))
class SimplePicker(QDialog):
def __init__(self, data, parent=None):
super().__init__(parent=parent)
# Get a grayscale image
bdata = ((data - data.min()) / (data.max() - data.min()) * 255).astype(np.uint8)
wid, hgt = bdata.shape
img = QImage(bdata.T.copy(), wid, hgt, wid,
QImage.Format_Indexed8)
# Construct a scene with pixmap
self.scene = QGraphicsScene(0, 0, wid, hgt, self)
self.scene.setSceneRect(0, 0, wid, hgt)
self.px = self.scene.addPixmap(QPixmap.fromImage(img))
# Construct the view and connect mouse clicks
self.view = MyView(self.scene, self)
self.view.mousedown.connect(self.mouse_click)
# End button
self.doneb = QPushButton('Done', self)
self.doneb.clicked.connect(self.accept)
# Layout
layout = QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.doneb)
#pyqtSlot(QPointF)
def mouse_click(self, xy):
print((xy.x(), xy.y()))
if __name__ == "__main__":
# Fake data
x, y = np.mgrid[0:4*np.pi:150j, 0:4*np.pi:200j]
z = np.sin(x) * np.sin(y)
qapp = QApplication.instance()
if qapp is None:
qapp = QApplication(['python'])
pick = SimplePicker(z)
print("Size is (150, 200)")
print("Putting in (50, 125) - This point should return (50.0, 75.0)")
p0 = QPoint(50, 125)
print("Before show():", pick.view.mapToScene(p0))
pick.show()
print("After show() :", pick.view.mapToScene(p0))
qapp.exec_()
This example is in PyQt5 on Windows, but PyQt4 on Linux does the same thing.
Upon diving into the C++ Qt source code, this is the Qt definition of mapToScene for a QPoint:
QPointF QGraphicsView::mapToScene(const QPoint &point) const
{
Q_D(const QGraphicsView);
QPointF p = point;
p.rx() += d->horizontalScroll();
p.ry() += d->verticalScroll();
return d->identityMatrix ? p : d->matrix.inverted().map(p);
}
The critical things there are the p.rx() += d->horizontalScroll(); and likewise vertical scroll. A QGraphicsView always contains scroll bars, even if they are always off or not shown. The offsets observed before the widget is shown are from the values of the horizontal and vertical scroll bars upon initialization, which must get modified to match the view/viewport when the widgets are shown and layouts calculated. In order for mapToScene to operate properly, the scroll bars must be set up to match the scene/view.
If I put the following lines put before the call to mapToScene in the example, then I get the appropriate transformation result without the necessity of showing the widget.
pick.view.horizontalScrollBar().setRange(0, 150)
pick.view.verticalScrollBar().setRange(-200, 0)
pick.view.horizontalScrollBar().setValue(0)
pick.view.verticalScrollBar().setValue(-200)
To do this more generally, you can pull some relevant transformations from the view.
# Use the size hint to get shape info
wid, hgt = (pick.view.sizeHint().width()-2,
pick.view.sizeHint().height()-2) # -2 removes padding ... maybe?
# Get the opposing corners through the view transformation
px = pick.view.transform().map(QPoint(wid, 0))
py = pick.view.transform().map(QPoint(0, hgt))
# Set the scroll bars accordingly
pick.view.horizontalScrollBar().setRange(px.y(), px.x())
pick.view.verticalScrollBar().setRange(py.y(), py.x())
pick.view.horizontalScrollBar().setValue(px.y())
pick.view.verticalScrollBar().setValue(py.y())
This is a hack-ish and ugly solution, so while it does work there may be a more elegant way to handle this.
have you tried implementing your own qgraphicsview and overriding your resizeEvent? When you mess around with mapTo"something" you gotta take care of your resizeEvents, have a look in this piece of code I've took from yours and modified a bit ><
from PyQt5.QtCore import QRectF
from PyQt5.QtWidgets import (QGraphicsScene, QGraphicsView, QVBoxLayout,
QApplication, QFrame, QSizePolicy)
from PyQt5.QtCore import QPoint
class GraphicsView(QGraphicsView):
def __init__(self):
super(GraphicsView, self).__init__()
# Scene and view
scene = QGraphicsScene(0, 0, 150, 200,)
scene.setSceneRect(0, 0, 150, 200)
def resizeEvent(self, QResizeEvent):
self.setSceneRect(QRectF(self.viewport().rect()))
qapp = QApplication(['python'])
# Just something to be a parent
view = GraphicsView()
# Short layout
# Make a test point
p0 = QPoint(50, 125)
# Pass in the test point before and after
print("Passing in point: ", p0)
print("Received point before show:", view.mapToScene(p0))
view.show()
print("Received point after show:", view.mapToScene(p0))
qapp.exec_()
Is that the behavior you wanted? ")

Categories

Resources