Source code for pyqtgraph.widgets.RemoteGraphicsView

from ..Qt import QtGui, QtCore, QT_LIB
if QT_LIB in ['PyQt4', 'PyQt5']:
    import sip
from .. import multiprocess as mp
from .GraphicsView import GraphicsView
from .. import CONFIG_OPTIONS
import numpy as np
import mmap, tempfile, ctypes, atexit, sys, random

__all__ = ['RemoteGraphicsView']
        
class SerializableWheelEvent:
    """
    Contains all information of a QWheelEvent, is serializable and can generate QWheelEvents.
    
    Methods have the functionality of their QWheelEvent equivalent.
    """
    def __init__(self, _pos, _globalPos, _delta, _buttons, _modifiers, _orientation):
        self._pos = _pos
        self._globalPos = _globalPos
        self._delta = _delta
        self._buttons = _buttons
        self._modifiers = _modifiers
        self._orientation_vertical = _orientation == QtCore.Qt.Vertical
        
    def pos(self):
        return self._pos

    def globalPos(self):
        return self._globalPos
    
    def delta(self):
        return self._delta
    
    def orientation(self):
        if self._orientation_vertical:
            return QtCore.Qt.Vertical
        else:
            return QtCore.Qt.Horizontal
    
    def angleDelta(self):
        if self._orientation_vertical:
            return QtCore.QPoint(0, self._delta)
        else:
            return QtCore.QPoint(self._delta, 0)

    def buttons(self):
        return QtCore.Qt.MouseButtons(self._buttons)
    
    def modifiers(self):
        return QtCore.Qt.KeyboardModifiers(self._modifiers)
    
    def toQWheelEvent(self):
        """
        Generate QWheelEvent from SerializableWheelEvent.
        """
        if QT_LIB in ['PyQt4', 'PySide']:
            return QtGui.QWheelEvent(self.pos(), self.globalPos(), self.delta(), self.buttons(), self.modifiers(), self.orientation())
        else:
            return QtGui.QWheelEvent(self.pos(), self.globalPos(), QtCore.QPoint(), self.angleDelta(), self.delta(), self.orientation(), self.buttons(), self.modifiers())

[docs]class RemoteGraphicsView(QtGui.QWidget): """ Replacement for GraphicsView that does all scene management and rendering on a remote process, while displaying on the local widget. GraphicsItems must be created by proxy to the remote process. """
[docs] def __init__(self, parent=None, *args, **kwds): """ The keyword arguments 'useOpenGL' and 'backgound', if specified, are passed to the remote GraphicsView.__init__(). All other keyword arguments are passed to multiprocess.QtProcess.__init__(). """ self._img = None self._imgReq = None self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView. ## without it, the widget will not compete for space against another GraphicsView. QtGui.QWidget.__init__(self) # separate local keyword arguments from remote. remoteKwds = {} for kwd in ['useOpenGL', 'background']: if kwd in kwds: remoteKwds[kwd] = kwds.pop(kwd) self._proc = mp.QtProcess(**kwds) self.pg = self._proc._import('pyqtgraph') self.pg.setConfigOptions(**CONFIG_OPTIONS) rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') self._view = rpgRemote.Renderer(*args, **remoteKwds) self._view._setProxyOptions(deferGetattr=True) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.setMouseTracking(True) self.shm = None shmFileName = self._view.shmFileName() if sys.platform.startswith('win'): self.shmtag = shmFileName else: self.shmFile = open(shmFileName, 'r') self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off')) ## Note: we need synchronous signals ## even though there is no return value-- ## this informs the renderer that it is ## safe to begin rendering again. for method in ['scene', 'setCentralItem']: setattr(self, method, getattr(self._view, method))
def resizeEvent(self, ev): ret = QtGui.QWidget.resizeEvent(self, ev) self._view.resize(self.size(), _callSync='off') return ret def sizeHint(self): return QtCore.QSize(*self._sizeHint) def remoteSceneChanged(self, data): w, h, size, newfile = data #self._sizeHint = (whint, hhint) if self.shm is None or self.shm.size != size: if self.shm is not None: self.shm.close() if sys.platform.startswith('win'): self.shmtag = newfile ## on windows, we create a new tag for every resize self.shm = mmap.mmap(-1, size, self.shmtag) ## can't use tmpfile on windows because the file can only be opened once. elif sys.platform == 'darwin': self.shmFile.close() self.shmFile = open(self._view.shmFileName(), 'r') self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) else: self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) self.shm.seek(0) data = self.shm.read(w*h*4) self._img = QtGui.QImage(data, w, h, QtGui.QImage.Format_ARGB32) self._img.data = data # data must be kept alive or PySide 1.2.1 (and probably earlier) will crash. self.update() def paintEvent(self, ev): if self._img is None: return p = QtGui.QPainter(self) p.drawImage(self.rect(), self._img, QtCore.QRect(0, 0, self._img.width(), self._img.height())) p.end() def mousePressEvent(self, ev): self._view.mousePressEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mousePressEvent(self, ev) def mouseReleaseEvent(self, ev): self._view.mouseReleaseEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mouseReleaseEvent(self, ev) def mouseMoveEvent(self, ev): self._view.mouseMoveEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mouseMoveEvent(self, ev) def wheelEvent(self, ev): delta = 0 orientation = QtCore.Qt.Horizontal if QT_LIB in ['PyQt4', 'PySide']: delta = ev.delta() orientation = ev.orientation() else: delta = ev.angleDelta().x() if delta == 0: orientation = QtCore.Qt.Vertical delta = ev.angleDelta().y() serializableEvent = SerializableWheelEvent(ev.pos(), ev.pos(), delta, int(ev.buttons()), int(ev.modifiers()), orientation) self._view.wheelEvent(serializableEvent, _callSync='off') ev.accept() return QtGui.QWidget.wheelEvent(self, ev) def keyEvent(self, ev): if self._view.keyEvent(int(ev.type()), int(ev.modifiers()), text, autorep, count): ev.accept() return QtGui.QWidget.keyEvent(self, ev) def enterEvent(self, ev): self._view.enterEvent(int(ev.type()), _callSync='off') return QtGui.QWidget.enterEvent(self, ev) def leaveEvent(self, ev): self._view.leaveEvent(int(ev.type()), _callSync='off') return QtGui.QWidget.leaveEvent(self, ev)
[docs] def remoteProcess(self): """Return the remote process handle. (see multiprocess.remoteproxy.RemoteEventHandler)""" return self._proc
[docs] def close(self): """Close the remote process. After this call, the widget will no longer be updated.""" self._proc.close()
class Renderer(GraphicsView): ## Created by the remote process to handle render requests sceneRendered = QtCore.Signal(object) def __init__(self, *args, **kwds): ## Create shared memory for rendered image #pg.dbg(namespace={'r': self}) if sys.platform.startswith('win'): self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) self.shm = mmap.mmap(-1, mmap.PAGESIZE, self.shmtag) # use anonymous mmap on windows else: self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') self.shmFile.write(b'\x00' * (mmap.PAGESIZE+1)) self.shmFile.flush() fd = self.shmFile.fileno() self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) atexit.register(self.close) GraphicsView.__init__(self, *args, **kwds) self.scene().changed.connect(self.update) self.img = None self.renderTimer = QtCore.QTimer() self.renderTimer.timeout.connect(self.renderView) self.renderTimer.start(16) def close(self): self.shm.close() if not sys.platform.startswith('win'): self.shmFile.close() def shmFileName(self): if sys.platform.startswith('win'): return self.shmtag else: return self.shmFile.name def update(self): self.img = None return GraphicsView.update(self) def resize(self, size): oldSize = self.size() GraphicsView.resize(self, size) self.resizeEvent(QtGui.QResizeEvent(size, oldSize)) self.update() def renderView(self): if self.img is None: ## make sure shm is large enough and get its address if self.width() == 0 or self.height() == 0: return size = self.width() * self.height() * 4 if size > self.shm.size(): if sys.platform.startswith('win'): ## windows says "WindowsError: [Error 87] the parameter is incorrect" if we try to resize the mmap self.shm.close() ## it also says (sometimes) 'access is denied' if we try to reuse the tag. self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) self.shm = mmap.mmap(-1, size, self.shmtag) elif sys.platform == 'darwin': self.shm.close() self.shmFile.close() self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') self.shmFile.write(b'\x00' * (size + 1)) self.shmFile.flush() self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_WRITE) else: self.shm.resize(size) ## render the scene directly to shared memory if QT_LIB in ['PySide', 'PySide2']: ch = ctypes.c_char.from_buffer(self.shm, 0) #ch = ctypes.c_char_p(address) self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32) else: address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) # different versions of pyqt have different requirements here.. try: self.img = QtGui.QImage(sip.voidptr(address), self.width(), self.height(), QtGui.QImage.Format_ARGB32) except TypeError: try: self.img = QtGui.QImage(memoryview(buffer(self.shm)), self.width(), self.height(), QtGui.QImage.Format_ARGB32) except TypeError: # Works on PyQt 4.9.6 self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32) self.img.fill(0xffffffff) p = QtGui.QPainter(self.img) self.render(p, self.viewRect(), self.rect()) p.end() self.sceneRendered.emit((self.width(), self.height(), self.shm.size(), self.shmFileName())) def mousePressEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) btn = QtCore.Qt.MouseButton(btn) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) return GraphicsView.mousePressEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) def mouseMoveEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) btn = QtCore.Qt.MouseButton(btn) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) return GraphicsView.mouseMoveEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) def mouseReleaseEvent(self, typ, pos, gpos, btn, btns, mods): typ = QtCore.QEvent.Type(typ) btn = QtCore.Qt.MouseButton(btn) btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) return GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) def wheelEvent(self, ev): return GraphicsView.wheelEvent(self, ev.toQWheelEvent()) def keyEvent(self, typ, mods, text, autorep, count): typ = QtCore.QEvent.Type(typ) mods = QtCore.Qt.KeyboardModifiers(mods) GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count)) return ev.accepted() def enterEvent(self, typ): ev = QtCore.QEvent(QtCore.QEvent.Type(typ)) return GraphicsView.enterEvent(self, ev) def leaveEvent(self, typ): ev = QtCore.QEvent(QtCore.QEvent.Type(typ)) return GraphicsView.leaveEvent(self, ev)