Create a FeaturePython object part II/fr: Difference between revisions

From FreeCAD Documentation
(Created page with "=== Écrire un ViewProvider ===")
(Created page with "Un fournisseur de vues est le composant d'un objet qui lui permet d'avoir une représentation visuelle dans l'interface graphique - en particulier dans la vue 3D. FreeCAD util...")
Line 124: Line 124:
=== Écrire un ViewProvider ===
=== Écrire un ViewProvider ===


A View Provider is the component of an object which allows it to have visual representation in the GUI - specifically in the 3D view. FreeCAD uses an application structure known as 'model-view', which is designed to separate the data (the 'model') from it's visual representation (the 'view'). If you've spent any time working with FreeCAD in Python, you'll likely already be aware of this through the use of two core Python modules: FreeCAD and FreeCADGui (often aliased as 'App' and 'Gui' repectively).
Un fournisseur de vues est le composant d'un objet qui lui permet d'avoir une représentation visuelle dans l'interface graphique - en particulier dans la vue 3D. FreeCAD utilise une structure d'application appelée 'vue du modèle' qui est conçue pour séparer les données (le 'modèle') de sa représentation visuelle (la «vue»). Si vous avez passé du temps à travailler avec FreeCAD en Python, vous en serez probablement déjà conscient grâce à l'utilisation de deux modules Python principaux: FreeCAD et FreeCADGui (souvent aliasés respectivement «App» et «Gui»).


Thus, our FeaturePython Box implementation also requires these elements. Thus far, we've focused purely on the 'model' portion, so now it's time to write the 'view'. Fortunately, most view implementations are simple and require little effort to write, at least to get started. Here's an example ViewProvider, borrowed and slightly modified from [https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/TemplatePyMod/FeaturePython.py]
Thus, our FeaturePython Box implementation also requires these elements. Thus far, we've focused purely on the 'model' portion, so now it's time to write the 'view'. Fortunately, most view implementations are simple and require little effort to write, at least to get started. Here's an example ViewProvider, borrowed and slightly modified from [https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/TemplatePyMod/FeaturePython.py]

Revision as of 13:44, 3 January 2020

Other languages:

App::FeaturePython vs. Part::FeaturePython

Jusqu'à présent, nous nous sommes concentrés sur les aspects internes d'une classe Python construite autour d'un objet FeaturePython - en particulier, un objet App::FeaturePython object.
Nous avons créé l'objet, défini certaines propriétés et ajouté des rappels d'événements au niveau du document qui permettent à notre objet de répondre à un recalcul de document avec la méthode execute().

Mais nous n'avons toujours pas de boîte.

Cependant, nous pouvons facilement en créer une en ajoutant seulement deux lignes.

Tout d'abord, ajoutez une nouvelle importation en haut du fichier: import Part

Ensuite, dans execute(), ajoutez la ligne suivante (vous pouvez supprimer l'instruction print()):

def execute():
    """
    Called on document recompute
    """

    Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height)

Ces commandes exécutent des scripts python fournis avec FreeCAD par défaut.

  • La méthode makeBox() génère une nouvelle forme de boîte.
  • L'appel englobant à Part.show() ajoute la forme à l'arborescence du document et la rend visible.

Si FreeCAD est ouvert, rechargez le module boîte et créez un nouvel objet boîte en utilisant box.create() (supprimez tous les objets boîte existants, juste pour garder les choses propres).

Remarquez comment une boîte apparaît immédiatement à l'écran: la méthode execute() est appelée immédiatement après la création de la boîte et nous forçons le document à recalculer à la fin de box.create().


Mais il y a un problème.

Cela devrait être assez évident. La boîte elle-même est représentée par un objet entièrement différent de notre objet FeaturePython. La raison en est que Part.show() crée un objet boîte séparé et l'ajoute au document. En fait, si vous accédez à votre objet FeaturePython et modifiez les dimensions, vous verrez une autre forme de boîte créée et l'ancienne laissée en place. Ce n'est pas bon! De plus, si la vue du rapport est ouverte, vous pouvez remarquer une erreur indiquant 'Les recalculs imbriqués d'un document ne sont pas autorisés'. Cela a à voir avec l'utilisation de la méthode Part.show() dans un objet FeaturePython. Nous voulons éviter de faire cela.

Truc

Le problème est que nous nous appuyons sur les méthodes Part.make*(), qui ne génèrent que des objets Part::Feature non paramétriques (formes simples et stupides), tout comme vous obtiendriez si vous copiez un objet paramétrique à l'aide de Part Copie simple.

Ce que nous voulons, bien sûr, est un objet boîte paramétrique qui redimensionne la boîte existante lorsque nous modifions ses propriétés. Nous pourrions supprimer l'objet Part::Feature précédent et le régénérer à chaque fois que nous modifions une propriété, mais nous avons encore deux objets à gérer - notre objet FeaturePython personnalisé et l'objet Part::Feature qu'il génère.

Alors, comment pouvons-nous résoudre ce problème?

Tout d'abord, nous devons utiliser le bon type d'objet.

Jusqu'à présent, nous avons utilisé des objets App::FeaturePython. Ils sont pas mal mais ils ne sont pas destinés à être utilisés comme objets de géométrie. Ils sont plutôt mieux utilisés comme objets de document qui ne nécessitent pas de représentation visuelle dans la vue 3D. Nous devons donc utiliser un objet Part::FeaturePython à la place.


Dans create(), modifiez la ligne suivante:

obj = App.ActiveDocument.addObject('App::FeaturePython', obj_name)

pour lire:

obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)

Pour terminer les modifications dont nous avons besoin, la ligne suivante de la méthode execute() doit être modifiée:

Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height))

en:

obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)

Votre code devrait ressembler à ceci:

import FreeCAD as App
import Part

def create(obj_name):
  """
  Object creation method
  """

  obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)

  fpo = box(obj)

  App.ActiveDocument.recompute()

  return fpo

class box():

  def __init__(self, obj):
      """
      Default Constructor
      """

      self.Type = 'box'

      obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = ""
      obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0
      obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width').Width = '10 mm'
      obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box height').Height = '1 cm'

      obj.Proxy = self
      self.Object = obj

  def execute(self, obj):
      """
      Called on document recompute
      """

      obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)

Maintenant, enregistrez vos modifications et revenez à FreeCAD. Supprimez tous les objets existants, rechargez le module de boîte et créez un nouvel objet de boîte.

Les résultats peuvent sembler un peu mitigés. L'icône dans l'arborescence est différente - c'est une boîte, maintenant. Mais il n'y a pas de cube. Et l'icône est toujours grise!

Qu'est-il arrivé? Bien que nous ayons correctement créé notre forme de boîte et l'avons affectée à un objet Part::FeaturePython, avant de pouvoir l'afficher dans notre vue 3D, nous devons attribuer un ViewProvider.

Écrire un ViewProvider

Un fournisseur de vues est le composant d'un objet qui lui permet d'avoir une représentation visuelle dans l'interface graphique - en particulier dans la vue 3D. FreeCAD utilise une structure d'application appelée 'vue du modèle' qui est conçue pour séparer les données (le 'modèle') de sa représentation visuelle (la «vue»). Si vous avez passé du temps à travailler avec FreeCAD en Python, vous en serez probablement déjà conscient grâce à l'utilisation de deux modules Python principaux: FreeCAD et FreeCADGui (souvent aliasés respectivement «App» et «Gui»).

Thus, our FeaturePython Box implementation also requires these elements. Thus far, we've focused purely on the 'model' portion, so now it's time to write the 'view'. Fortunately, most view implementations are simple and require little effort to write, at least to get started. Here's an example ViewProvider, borrowed and slightly modified from [1]

class ViewProviderBox:
   def __init__(self, obj):
       """
       Set this object to the proxy object of the actual view provider
       """
       obj.Proxy = self

   def attach(self, obj):
       """
       Setup the scene sub-graph of the view provider, this method is mandatory
       """
       return

   def updateData(self, fp, prop):
       """
       If a property of the handled feature has changed we have the chance to handle this here
       """
       return

   def getDisplayModes(self,obj):
       """
       Return a list of display modes.
       """
       return None

   def getDefaultDisplayMode(self):
       """
       Return the name of the default display mode. It must be defined in getDisplayModes.
       """
       return "Shaded"

   def setDisplayMode(self,mode):
       """
       Map the display mode defined in attach with those defined in getDisplayModes.
       Since they have the same names nothing needs to be done.
       This method is optional.
       """
       return mode

   def onChanged(self, vp, prop):
       """
       Print the name of the property that has changed
       """

       App.Console.PrintMessage("Change property: " + str(prop) + "\n")

   def getIcon(self):
       """
       Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown.
       """

       return """
           /* XPM */
           static const char * ViewProviderBox_xpm[] = {
           "16 16 6 1",
           " 	c None",
           ".	c #141010",
           "+	c #615BD2",
           "@	c #C39D55",
           "#	c #000000",
           "$	c #57C355",
           "        ........",
           "   ......++..+..",
           "   .@@@@.++..++.",
           "   .@@@@.++..++.",
           "   .@@  .++++++.",
           "  ..@@  .++..++.",
           "###@@@@ .++..++.",
           "##$.@@$#.++++++.",
           "#$#$.$$$........",
           "#$$#######      ",
           "#$$#$$$$$#      ",
           "#$$#$$$$$#      ",
           "#$$#$$$$$#      ",
           " #$#$$$$$#      ",
           "  ##$$$$$#      ",
           "   #######      "};
           """

   def __getstate__(self):
       """
       Called during document saving.
       """
       return None

   def __setstate__(self,state):
       """"
       Called during document restore.
       """"
       return None

Note in the above code, we also define an XMP icon for this object. Icon design is beyond the scope of this tutorial, but basic icon designs can be managed using open source tools like GIMP, Krita, and Inkscape. The getIcon method is optional, as well. If it is not provided, FreeCAD will provide a default icon.


With out ViewProvider defined, we now need to put it to use to give our object the gift of visualization.

Return to the create() method in your code and add the following near the end:

ViewProviderBox(obj.ViewObject)

This instances the custom ViewProvider class and passes the FeaturePython's built-in ViewObject to it. The ViewObject won't do anything without our custom class implementation, so when the ViewProvider class initializes, it saves a reference to itself in the FeaturePython's ViewObject.Proxy attribute. This way, when FreeCAD needs to render our Box visually, it can find the ViewProvider class to do that.

Now, save the changes and return to FreeCAD. Import or reload the Box module and call box.create().

We still don't see anything, but notice what happened to the icon next to the box object. It's in color! And it has a shape! That's a clue that our ViewProvider is working as expected.

So now, it's time to actually *add* a box.

Return to the code and add the following line to the execute() method:

obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)

Now save, reload the box module in FreeCAD and create a new box.

You should see a box appear on screen! If it's too big (or maybe doesn't show up at all), click one of the 'ViewFit' buttons to fit it to the 3D view.

Note what we did here that differs from the way it was implemented for App::FeaturePython above. In the execute() method, we called the Part.makeBox() macro and passed it the box properties as before. But rather than call Part.show() on the resulting object (which created a new, separate box object), we simply assigned it to the Shape property of our Part::FeaturePython object instead.

You can even alter the box dimensions by changing the values in the FreeCAD property panel. Give it a try!

Trapping Events

To this point, we haven't explicitly addressed event trapping. Nearly every method of a FeaturePython class serves as a callback accessible to the FeaturePython object (which gets access to our class instance through the Proxy attribute, if you recall).

Below is a list of the callbacks that may be implemented in the basic FeaturePython object:

FeaturePython basic callbacks
execute(self, obj) Called during document recomputes Do not call recompute() from this method (or any method called from execute()) as this causes a nested recompute.
onBeforeChanged(self, obj, prop) Called before a property value is changed prop is the name of the property to be changed, not the property object itself. Property changes cannot be cancelled. Previous / next property values are not simultaneously available for comparison.
onChanged(self, obj, prop) Called after a property is changed prop is the name of the property to be changed, not the property object itself.
onDocumentRestored(self, obj) Called after a document is restored or aFeaturePython object is copied / duplicated. Occasionally, references to the FeaturePython object from the class, or the class from the FeaturePython object may be broken, as the class __init__() method is not called when the object is reconstructed. Adding {{{1}}} or {{{1}}} often solves these issues.

In addition, there are two callbacks in the ViewProvider class that may occasionally prove useful:

ViewProvider basic callbacks
updateData(self, obj, prop) Called after a data (model) property is changed obj is a reference to the FeaturePython class instance, not the ViewProvider instance. prop is the name of the property to be changed, not the property object itself.
onChanged(self, vobj, prop) Called after a view property is changed vobj is a reference to the ViewProvider instance. prop is the name of the view property which was changed.


Tip
It is not uncommon to encounter a situation where the Python callbacks are not being triggered as they should. Beginners in this area need to rest assured that the FeaturePython callback system is not fragile or broken. Invariably, when callbacks fail to run, it is because a reference is lost or undefined in the underlying code. If, however, callbacks appear to be breaking with no explanation, providing object / proxy references in the onDocumentRestored() callback (as noted in the first table above) may alleviate these problems. Until you are comfortable with the callback system, it may be useful to add print statements in each callback to print messages to the console as a diagnostic during development.

The Code

The complete code for this example:

import FreeCAD as App
import Part

def create(obj_name):
   """
   Object creation method
   """

   obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)

   fpo = box(obj)

   ViewProviderBox(obj.ViewObject)

   App.ActiveDocument.recompute()

   return fpo


class box():

   def __init__(self, obj):
       """
       Default Constructor
       """

       self.Type = 'box'
       self.ratios = None

       obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = 'Hello World!'
       obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0
       obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width'). Width = '10 mm'
       obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box Height').Height = '1 cm'
       obj.addProperty('App::PropertyBool', 'Aspect Ratio', 'Dimensions', 'Lock the box aspect ratio').Aspect_Ratio = False

       obj.Proxy = self
       self.Object = obj

   def __getstate__(self):
       return self.Type

   def __setstate__(self, state):
       if state:
           self.Type = state
 
   def execute(self, obj):
       """
       Called on document recompute
       """

       obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)


class ViewProviderBox:
   def __init__(self, obj):
       """
       Set this object to the proxy object of the actual view provider
       """
       obj.Proxy = self

   def attach(self, obj):
       """
       Setup the scene sub-graph of the view provider, this method is mandatory
       """
       return

   def updateData(self, fp, prop):
       """
       If a property of the handled feature has changed we have the chance to handle this here
       """

       return

   def getDisplayModes(self,obj):
       """
       Return a list of display modes.
       """
       return None

   def getDefaultDisplayMode(self):
       """
       Return the name of the default display mode. It must be defined in getDisplayModes.
       """
       return "Shaded"

   def setDisplayMode(self,mode):
       """
       Map the display mode defined in attach with those defined in getDisplayModes.
       Since they have the same names nothing needs to be done.
       This method is optional.
       """
       return mode

   def onChanged(self, vobj, prop):
       """
       Print the name of the property that has changed
       """

       App.Console.PrintMessage("Change property: " + str(prop) + "\n")

   def getIcon(self):
       """
       Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown.
       """

       return """
           /* XPM */
           static const char * ViewProviderBox_xpm[] = {
           "16 16 6 1",
           " 	c None",
           ".	c #141010",
           "+	c #615BD2",
           "@	c #C39D55",
           "#	c #000000",
           "$	c #57C355",
           "        ........",
           "   ......++..+..",
           "   .@@@@.++..++.",
           "   .@@@@.++..++.",
           "   .@@  .++++++.",
           "  ..@@  .++..++.",
           "###@@@@ .++..++.",
           "##$.@@$#.++++++.",
           "#$#$.$$$........",
           "#$$#######      ",
           "#$$#$$$$$#      ",
           "#$$#$$$$$#      ",
           "#$$#$$$$$#      ",
           " #$#$$$$$#      ",
           "  ##$$$$$#      ",
           "   #######      "};
           """

   def __getstate__(self):
       return None

   def __setstate__(self,state):
       return None