Embedding a view to another (QT) application?

Have some feature requests, feedback, cool stuff to share, or want to know where FreeCAD is going? This is the place.
Forum rules
Be nice to others! Read the FreeCAD code of conduct!
wmayer
Site Admin
Posts: 16455
Joined: Thu Feb 19, 2009 10:32 am

Re: Embedding a view to another (QT) application?

Postby wmayer » Sun Dec 27, 2009 9:06 pm

This is now an example which works quite well (on Windows & Ubuntu).

Code: Select all


import os
import sys

from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QMainWindow, QWorkspace, QAction, QFileDialog, QApplication

from pivy.coin import SoInput, SoDB
from pivy import sogui
import FreeCAD, FreeCADGui

def getMainWindow():
   toplevel = QtGui.qApp.topLevelWidgets()
   for i in toplevel:
      if i.metaObject().className() == "Gui::MainWindow":
         return i
   raise Exception("No main window found")

class MdiMainWindow(QMainWindow):
    def __init__(self, qApp):
        QMainWindow.__init__(self)
        self._firstwidget = None
        self._workspace = QWorkspace()
        self.setCentralWidget(self._workspace)
        self.setAcceptDrops(True)
        self.setWindowTitle("Pivy Quarter MDI example")
        self.viewers=[]

        filemenu = self.menuBar().addMenu("&File")
        windowmenu = self.menuBar().addMenu("&Windows")

        fileopenaction = QAction("&Create Box", self)
        fileexitaction = QAction("E&xit", self)
        tileaction = QAction("Tile", self)
        cascadeaction = QAction("Cascade", self)

        filemenu.addAction(fileopenaction)
        filemenu.addAction(fileexitaction)
        windowmenu.addAction(tileaction)
        windowmenu.addAction(cascadeaction)

        self.connect(fileopenaction, QtCore.SIGNAL("triggered()"), self.createBoxInFreeCAD)
        self.connect(fileexitaction, QtCore.SIGNAL("triggered()"), QtGui.qApp.closeAllWindows)
        self.connect(tileaction, QtCore.SIGNAL("triggered()"), self._workspace.tile)
        self.connect(cascadeaction, QtCore.SIGNAL("triggered()"), self._workspace.cascade)

        windowmapper = QtCore.QSignalMapper(self)
        self.connect(windowmapper, QtCore.SIGNAL("mapped(QWidget *)"), self._workspace.setActiveWindow)

    def closeEvent(self, event):
        self._workspace.closeAllWindows()

    def createBoxInFreeCAD(self):
        widget = QtGui.QWidget(self._firstwidget)
        viewer = sogui.SoGuiExaminerViewer(widget)
        self._workspace.addWindow(widget)
        if not self._firstwidget:
            self._firstwidget = widget
        widget.show()
        self.viewers.append(viewer)
        doc = FreeCAD.newDocument()
        doc.addObject("Part::Box","myBox")
        doc.recompute()
        iv_=FreeCADGui.getDocument(doc.Name).getObject("myBox").toString()
        in_ = SoInput()
        in_.setBuffer(iv_)
        root = SoDB.readAll(in_)
        if (root):
            viewer.setSceneGraph(root)


def main():
    app = QApplication(sys.argv)
    mdi = MdiMainWindow(app)    
    mdi.show()
    FreeCADGui.showMainWindow() # setup the GUI stuff of FreeCAD
    mw=getMainWindow()
    mw.hide() # hide all
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
User avatar
yorik
Site Admin
Posts: 12034
Joined: Tue Feb 17, 2009 9:16 pm
Location: Brussels, Belgium
Contact:

Re: Embedding a view to another (QT) application?

Postby yorik » Mon Dec 28, 2009 12:58 am

wmayer wrote:

Code: Select all

iv_=FreeCADGui.getDocument(doc.Name).getObject("myBox").toString()
        in_ = SoInput()
        in_.setBuffer(iv_)
        root = SoDB.readAll(in_)
That's the method I used before in the Draft module, but I found it pretty unstable (quite often, the string gets read "incompletely" into the buffer)... So now I use something like this:

Code: Select all

root = FreeCADGui.getObject("myBox").RootNode.copy()
Tomorrow I'll have a look if it works in this example.
wmayer
Site Admin
Posts: 16455
Joined: Thu Feb 19, 2009 10:32 am

Re: Embedding a view to another (QT) application?

Postby wmayer » Mon Dec 28, 2009 11:47 am

Thanks for the hint Yorik. The new method to get the Inventor structure of an object is called subgraphFromObject which returns an SoNode which can be directly added to the scenegraph.

I have prepared the FreeCADGui module to be used completely without GUI or eventloop. Therefore, I have added the method setupWithoutGUI which must be called once after importing the module.

Here is again a slightly modified version of the above script.

Code: Select all


import os
import sys

from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QMainWindow, QWorkspace, QAction, QFileDialog, QApplication

from pivy.coin import SoInput, SoDB
from pivy import sogui
import FreeCAD, FreeCADGui

class MdiMainWindow(QMainWindow):
    def __init__(self, qApp):
        QMainWindow.__init__(self)
        self._firstwidget = None
        self._workspace = QWorkspace()
        self.setCentralWidget(self._workspace)
        self.setAcceptDrops(True)
        self.setWindowTitle("Pivy Quarter MDI example")
        self.viewers=[]

        filemenu = self.menuBar().addMenu("&File")
        windowmenu = self.menuBar().addMenu("&Windows")

        fileopenaction = QAction("&Create Box", self)
        fileexitaction = QAction("E&xit", self)
        tileaction = QAction("Tile", self)
        cascadeaction = QAction("Cascade", self)

        filemenu.addAction(fileopenaction)
        filemenu.addAction(fileexitaction)
        windowmenu.addAction(tileaction)
        windowmenu.addAction(cascadeaction)

        self.connect(fileopenaction, QtCore.SIGNAL("triggered()"), self.createBoxInFreeCAD)
        self.connect(fileexitaction, QtCore.SIGNAL("triggered()"), QtGui.qApp.closeAllWindows)
        self.connect(tileaction, QtCore.SIGNAL("triggered()"), self._workspace.tile)
        self.connect(cascadeaction, QtCore.SIGNAL("triggered()"), self._workspace.cascade)

        windowmapper = QtCore.QSignalMapper(self)
        self.connect(windowmapper, QtCore.SIGNAL("mapped(QWidget *)"), self._workspace.setActiveWindow)

    def closeEvent(self, event):
        self._workspace.closeAllWindows()

    def createBoxInFreeCAD(self):
        widget = QtGui.QWidget(self._firstwidget)
        viewer = sogui.SoGuiExaminerViewer(widget)
        self._workspace.addWindow(widget)
        if not self._firstwidget:
            self._firstwidget = widget
        widget.show()
        self.viewers.append(viewer)
        doc = FreeCAD.newDocument()
        obj=doc.addObject("Part::Box","myBox")
        doc.recompute()
        root=FreeCADGui.subgraphFromObject(obj)
        viewer.setSceneGraph(root)


def main():
    app = QApplication(sys.argv)
    mdi = MdiMainWindow(app)    
    mdi.show()
    FreeCADGui.setupWithoutGUI()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
jukkaaho
Posts: 14
Joined: Thu Dec 24, 2009 5:42 am

Re: Embedding a view to another (QT) application?

Postby jukkaaho » Mon Dec 28, 2009 1:15 pm

That's great news to hear! If FreeCAD project truly want's to separate GUI and App to different parts that can be used separately AND effectively, they both should give full advantage of using them in different projects. That's the power of design. I think the point is still that when the user needs GUI features, he/she imports FreeCADGui, and when he/she needs just something non-gui features to use in own side-projects, fe. embedded view, that should also be possible. Like scenegraph. And when talking about software design philosophy, also vice versa. The user should take the full advantage of GUI features without using the core app at all.

I have to argue this a little bit more, but i still think that exporting scenegraph isn't basically GUI feature at all and it shouldn't even be in GUI module. It's just a piece of data that gives information to GUI what should it be drawing. They're instructions to GUI, not actually vector of circles what are drawn to display. Nevertheless, scenegraph is very tightly migrated to GUI. The question is then where should it be then, if not in GUI? I think that this kind of methods, what are the "glue" between the GUI and core App, are interfaces that can communicate to both modules. And even to 3rd party modules, when needed. In ideal situation there shouldn't be very much logic in GUI part of application. It just takes the commands from the user to the model and represents the model's data in human readable format to user.

BTW, that example is still not working on Mac. I don't know if it's some kind of OS X specific feature, but here's the error anyway:

Code: Select all

Assertion failed: (SoFCSelection::classTypeId != SoType::badType() && "you forgot init()!"), function SoFCSelection, file SoFCSelection.cpp, line 74.
Abnormal program termination...
Are you sure that code isn't launching another graphics handler to root when calling FreeCAD.newDocument()? That error crashed even interpreter, so there is something horribly wrong, because Python's own exception class cannot handle the situation properly.

Jukka Aho
User avatar
jriegel
Site Admin
Posts: 3369
Joined: Sun Feb 15, 2009 5:29 pm
Location: Ulm, Germany
Contact:

Re: Embedding a view to another (QT) application?

Postby jriegel » Mon Dec 28, 2009 1:23 pm

Your argumentation would be right if we would generally use a independent graphic representation. But in FreeCAD the ViewProviders translate from the (App)data to a (Coin) scene graph and need there fore
access to Coin and the application layer. A independent middle representation would be terrible slow and a lot work to implement! Here you have always the trade off of a (to) flexible design and performance.
Stop whining - start coding!
User avatar
yorik
Site Admin
Posts: 12034
Joined: Tue Feb 17, 2009 9:16 pm
Location: Brussels, Belgium
Contact:

Re: Embedding a view to another (QT) application?

Postby yorik » Mon Dec 28, 2009 2:15 pm

I also think that taking FreeCAD's 3D view and putting it inside another application would be something pretty heavy to do and with little use... I mean, why would you want a complete, working "FreeCAD-way" 3D view, with all its side functionality (selection, display modes, etc...) but outside FreeCAD? Basically you would need 90% of FreeCAD, so why not use FreeCAD itself?

Werner's last improvements here is really something huge. You can now think of FreeCAD as being able to pipe out its graphical output to a custom viewer... That's quite a revolution! I actually know no other 3D app able to do that. And by piping the coin output into a "translator", imagine what crazy results are possible... And now if you simply don't like the way the FreeCAD GUI works, you can easily imagine your own...

I'm updating the wiki page with latest improvements. Well done guys!
wmayer
Site Admin
Posts: 16455
Joined: Thu Feb 19, 2009 10:32 am

Re: Embedding a view to another (QT) application?

Postby wmayer » Mon Dec 28, 2009 2:41 pm

Did you use the very latest sample script? Just in case your pivy installation uses the new Quarter GUI as default (as it is on Debian testing) the script below should work for you. I have also made some changes to setup properly the subsystem of FreeCAD which should solve the assertion failure.

Code: Select all

#!/usr/bin/env python

import os
import sys

from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QMainWindow, QWorkspace, QAction, QApplication

from pivy.coin import SoInput, SoDB
from pivy.quarter import QuarterWidget
import FreeCADGui


class MdiQuarterWidget(QuarterWidget):
    def __init__(self, parent, sharewidget):
        QuarterWidget.__init__(self, parent=parent, sharewidget=sharewidget)

    def minimumSizeHint(self):
        return QtCore.QSize(640, 480)


class MdiMainWindow(QMainWindow):
    def __init__(self, qApp):
        QMainWindow.__init__(self)
        self._firstwidget = None
        self._workspace = QWorkspace()
        self.setCentralWidget(self._workspace)
        self.setAcceptDrops(True)
        self.setWindowTitle("Pivy Quarter MDI example")

        filemenu = self.menuBar().addMenu("&File")
        windowmenu = self.menuBar().addMenu("&Windows")

        fileopenaction = QAction("&Create Box", self)
        fileexitaction = QAction("E&xit", self)
        tileaction = QAction("Tile", self)
        cascadeaction = QAction("Cascade", self)

        filemenu.addAction(fileopenaction)
        filemenu.addAction(fileexitaction)
        windowmenu.addAction(tileaction)
        windowmenu.addAction(cascadeaction)

        self.connect(fileopenaction, QtCore.SIGNAL("triggered()"), self.createBoxInFreeCAD)
        self.connect(fileexitaction, QtCore.SIGNAL("triggered()"), QtGui.qApp.closeAllWindows)
        self.connect(tileaction, QtCore.SIGNAL("triggered()"), self._workspace.tile)
        self.connect(cascadeaction, QtCore.SIGNAL("triggered()"), self._workspace.cascade)

        windowmapper = QtCore.QSignalMapper(self)
        self.connect(windowmapper, QtCore.SIGNAL("mapped(QWidget *)"), self._workspace.setActiveWindow)

        self.dirname = os.curdir        

    def closeEvent(self, event):
        self._workspace.closeAllWindows()

    def createBoxInFreeCAD(self):
        d=FreeCAD.newDocument()
        o=d.addObject("Part::Box")
        d.recompute()
        s=FreeCADGui.subgraphFromObject(o)
        child = self.createMdiChild()
        child.show()
        child.setSceneGraph(s)

    def createMdiChild(self):
        widget = MdiQuarterWidget(None, self._firstwidget)
        self._workspace.addWindow(widget)
        if not self._firstwidget:
            self._firstwidget = widget
        return widget


def main():
    FreeCADGui.setupWithoutGUI()
    app = QApplication(sys.argv)
    mdi = MdiMainWindow(app)    
    mdi.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
There is an issue with the 3d canvas which is however definitely not related to FreeCAD because this also happens even if not FreeCAD at all.
jukkaaho
Posts: 14
Joined: Thu Dec 24, 2009 5:42 am

Re: Embedding a view to another (QT) application?

Postby jukkaaho » Tue Dec 29, 2009 1:08 pm

Super. Now it works like charm. Here's another example of embedding FreeCAD to Qt

Code: Select all

# -*- coding: utf-8 -*-
"""
Created on Tue Dec 29 08:49:32 2009

@author: -
"""

#!/usr/bin/env python

import sys

from PyQt4 import QtCore, QtGui
from pivy import quarter as Quarter
import FreeCADGui
import FreeCAD
from FreeCAD import Part

class FreeCADModel(Quarter.QuarterWidget):
    def __init__(self, parent = None):
        super(FreeCADModel, self).__init__(parent)

        d = FreeCAD.newDocument()
        shape = self.drawShape()
        Part.show(shape)
        obj = d.ActiveObject
        d.recompute()
        self.model = FreeCADGui.subgraphFromObject(obj)
        self.setSceneGraph(self.model)

    def drawShape(self):
        raise AssertionError

    def minimumSizeHint(self):
        return QtCore.QSize(640, 480)

class MainWindow(QtGui.QMainWindow):
    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)
        self.canvas = OurGreatModel() # Our model widget
        self.setCentralWidget(self.canvas)
        self.setWindowTitle("Embedding FreeCAD model to Qt example")

### SHAPE ###

class OurGreatModel(FreeCADModel):
    def drawShape(self):
        from FreeCAD import Vector
        from Part import makeBox
        box1 = makeBox(10,10,10)
        box2 = makeBox(8,8,1,Vector(1,1,9))
        box3 = makeBox(8,8,1,Vector(1,1,0))
        box4 = makeBox(1,8,8,Vector(0,1,1))
        box5 = makeBox(1,8,8,Vector(9,1,1))
        box6 = makeBox(8,1,8,Vector(1,0,1))
        box7 = makeBox(7.999,1,8,Vector(1,9,1))
        shape = box1.cut(box2).cut(box3).cut(box4).cut(box5).cut(box6).cut(box7)
        return shape

def main():
    FreeCADGui.setupWithoutGUI()
    app = QtGui.QApplication(sys.argv)
    mw = MainWindow()
    mw.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
Jukka Aho
wmayer
Site Admin
Posts: 16455
Joined: Thu Feb 19, 2009 10:32 am

Re: Embedding a view to another (QT) application?

Postby wmayer » Thu Dec 31, 2009 10:48 am

Hi,

I have implmented the method writeInventor for the Python classes for points, meshes and shapes. Here is a full example how to use with points. Note, this approach is now 100%-GUI-free, i.e. FreeCADGui doesn't need to be loaded therefore.

Code: Select all

import Mesh
m=Mesh.createTorus()
l=[]
p=m.Points
for i in p:
		l.append(i.Vector)

import Points
p=Points.Points()
p.addPoints(l)
s=p.writeInventor()
from pivy.coin import SoDB, SoInput
inp=SoInput()
inp.setBuffer(s)
node=SoDB.readAll(inp)
App.newDocument()
Gui.ActiveDocument.ActiveView.getSceneGraph().addChild(node)
Remarks:
* The Points.writeInventor() doesn't accept any arguments
* Mesh.writeInventor() has an optional double argument which defines the "crease angle" (default: 0). The value is given in radians and can be used to get a smoother appearance. A good value therefore is 0.5 (~30 degree)
* Shape.writeInventor() has three optional arguments:
integer = {0,1,2}, 0: shaded, 1: wireframe, 2: shaded+wireframe
double: deviation parameter for the meshing of the shape
double: crease angle like for meshes

Werner
User avatar
yorik
Site Admin
Posts: 12034
Joined: Tue Feb 17, 2009 9:16 pm
Location: Brussels, Belgium
Contact:

Re: Embedding a view to another (QT) application?

Postby yorik » Thu Dec 31, 2009 12:42 pm

Wow good job! I think this will be pretty useful with python features...
I'll have a try and add a bit about this to the wiki