Scorpion Vision Software

Python Plugin Interface Specification

March 18, 2011

Reference : GD-2011-0008-A

Author : Thor Vollset and Roar Larsen

Tordivel AS


1. Introduction

The Scorpion Python Plugin Interface is defined to be able to used python and .Net as tightly integrated extentions to the Scorpion Vision Software.

The specification was defined in February 2011 to be able to defined a large number of graphical elements to easily extend Scorpion Vision Software 9.0. The first work on plugin were started in 2009.

1.1 Config Property

The new components shall manage it persistence in the same manner as the Standard ActiveX Plugins with getConfig and setConfig methods. Configuration as text. The suggested text format is Scorpion SPB format.

1.2 Naming conventions

Plugins should follow the global python world coding standards

1.2 Revisions

2. Plugin Interface Definition

2.1 Standard Construction Methods

The primary purpose is to create protected scope for the plugin.

    def <name>(<params>):

       <imports >

       < object definition >

       return plugin instance

See sample plugin object in section 4.


2.2 Standard Methods

    executeCmd(command,parameters)        # return true = success else fail

    configure()                 # return true if config is changed  

                                    # open configuration dialogue

3. Scorpion  API

The following functions shall be available for the Plugins  

Reference to Scorpion Online Help - TBD

4. Python Architecture for Plugins

The plugin template is defined in this section.

4.1 Plugin template

class MyPlugin(object):

  '''

  MyPlugin - sample plugin template

  Version 1.0

  TORDIVEL AS, roar@tordivel.no

  '''

  def __init__(self,name):

    self.name=name

    import sys

    if 'Scorpion' in sys.modules:

      RegisterCallback('System.ImageComplete',self.onImageComplete)

      RegisterCallback('System.Timer',self.onTimer)

      RegisterCallback('System.MouseMove',self.onMouseMove)

      RegisterCallback('System.MouseDown',self.onMouseDown)

      RegisterCallback('System.MouseUp',self.onMouseUp)

    else:

      RegisterCallback('OnInit',self.onInit)

      RegisterCallback('OnClose',self.onClose)

      RegisterCallback('OnImageComplete',self.onImageComplete)

      RegisterCallback('OnImage',self.onImage)

      RegisterCallback('OnTimer',self.onTimer)

      RegisterCallback('OnMouseMove',self.onMouseMove)

      RegisterCallback('OnMouseDown',self.onMouseDown)

      RegisterCallback('OnMouseUp',self.onMouseUp)

  def __str__(self):

    '''

    return unique plugin instance name

    '''

    return '%s_%s'%(self.__class__.__name__,self.name)

  def getConfig(self):

    '''

    return plugin configuration as string

    '''

    import SPB

    spb=SPB.CreateSpb()

    #TODO: insert configuration into spb

    spb.setInt('cfgver',1) #use cfgver to handle plugin compatibility

    return spb.xml

  def setConfig(self,value):

    '''

    set plugin configuration from string

    '''

    import SPB

    spb=SPB.CreateSpb(value)

    #TODO: extract configuration from spb

    if spb.getInt('cfgver')<>1:

      print '%s: Invalid config version'%self

  def configure(self):

    '''

    launch plugin configuration dialog,

    return True/False if configuratrion changed

    '''

    return False

  def onInit(self):

    print '%s.onInit()'%(self)

  def onClose(self):

    print '%s.onClose()'%(self)

  def onImageComplete(self,image):

    print '%s.onImageComplete(%s)'%(self,image)

  def onImage(self):

    print '%s.onImage()'%(self)

  def onTimer(self,timer):

    print '%s.onTimer(%s)'%(self,timer)

  def onMouseMove(self,image,shift,x,y):

    print '%s.onMouseMove(%s,%i,%f,%f)'%(self,image,shift,x,y)

  def onMouseDown(self,image,shift,x,y):

    print '%s.onMouseDown(%s,%i,%f,%f)'%(self,image,shift,x,y)

  def onMouseUp(self,image,shift,x,y):

    print '%s.onMouseUp(%s,%i,%f,%f)'%(self,image,shift,x,y)

4.2 Logo sample

This section shows the principal structure of the construction methods used to define scope to make the plugin complete without sideeffects.

##########################################################################

#

# LogoPlugin.py

# Plugin Sample

#

# Version 1.0.0.2

# 2011-02-02

#

##########################################################################

# Revision history

#

# 1.0.0.1 - 02feb2011: Initial version

# 1.0.0.2 - 19mar2011:

  # Converted to new Plugin Architecture

  # Use Python Documentation Standard for comments

def CreatePlugin(hWnd,name=''):

  '''

  standard plugin creation

  '''

  import os

  import types

  import SPB

  import dotNetUtils

  try:    import clr as CLR

  except: import CLR

  import CLR.System.Windows.Forms as WinForms

  from CLR.System.Reflection import Assembly

  Assembly.LoadFrom(os.path.join(os.path.dirname(os.path.realpath(__file__)), "DynamicPropertyClass.dll"))

  from CLR.PythonHelpers import DynamicPropertyClass

  from CLR.PythonHelpers import CustomProperty

  from Scorpion import GetStringValue

  from Scorpion import GetBoolValue

  from Scorpion import PluginChanged

  class LogoPanel(WinForms.Panel):

    '''

    LogoPanel 1.1 - .net plugin panel showing an image - typically logo

    TORDIVEL AS, 18mar2011, roar@tordivel.no

    '''

    class PropertiesDialog(WinForms.Form):

      '''

      configuration dialog for LogonPanel

      '''

      def __init__(self):

        self.dataObject = None

        self.okButton = WinForms.Button()

        self.cancelButton = WinForms.Button()

        self.propertyGrid = WinForms.PropertyGrid()

        self._initializeControls()

      def _initializeControls(self):

        self.SuspendLayout()

        # okButton

        self.okButton.Anchor = WinForms.AnchorStyles.Top + WinForms.AnchorStyles.Right

        self.okButton.Location = CLR.System.Drawing.Point(205, 12)

        self.okButton.Name = "okButton"

        self.okButton.Size = CLR.System.Drawing.Size(75, 23)

        self.okButton.TabIndex = 0

        self.okButton.Text = "OK"

        self.okButton.UseVisualStyleBackColor = True

        self.okButton.DialogResult = WinForms.DialogResult.OK;

        # cancelButton

        self.cancelButton.Anchor = WinForms.AnchorStyles.Top + WinForms.AnchorStyles.Right

        self.cancelButton.DialogResult = WinForms.DialogResult.Cancel

        self.cancelButton.Location = CLR.System.Drawing.Point(205, 42)

        self.cancelButton.Name = "cancelButton"

        self.cancelButton.Size = CLR.System.Drawing.Size(75, 23)

        self.cancelButton.TabIndex = 1

        self.cancelButton.Text = "Cancel"

        self.cancelButton.UseVisualStyleBackColor = True

        # propertyGrid

        self.propertyGrid.Anchor = WinForms.AnchorStyles.Top + WinForms.AnchorStyles.Bottom + WinForms.AnchorStyles.Left + WinForms.AnchorStyles.Right

        self.propertyGrid.Location = CLR.System.Drawing.Point(13, 12)

        self.propertyGrid.Name = "propertyGrid"

        self.propertyGrid.Size = CLR.System.Drawing.Size(186, 249)

        self.propertyGrid.TabIndex = 2

        self.propertyGrid.PropertyValueChanged += self._onPropertyValueChanged

        # SettingsForm

        self.AcceptButton = self.okButton

        self.AutoScaleMode = WinForms.AutoScaleMode.Font

        self.CancelButton = self.cancelButton

        self.ClientSize = CLR.System.Drawing.Size(292, 273)

        self.Controls.Add(self.propertyGrid)

        self.Controls.Add(self.cancelButton)

        self.Controls.Add(self.okButton)

        self.Name = "SettingsForm"

        self.StartPosition = WinForms.FormStartPosition.CenterParent

        self.Text = "Settings"

        self.ResumeLayout(False)

        self.Size = CLR.System.Drawing.Size(640, 480)

      def _onPropertyValueChanged(self, sender, propertyValueChangedEventArgs):

        if isinstance(self.dataObject, types.DictionaryType):

          if self.dataObject.has_key(propertyValueChangedEventArgs.ChangedItem.Label):

            newValue = propertyValueChangedEventArgs.ChangedItem.Value

            oldValue = self.dataObject[propertyValueChangedEventArgs.ChangedItem.Label]

            try:

              if isinstance(oldValue, str):

                newValue = str(newValue)

              elif isinstance(oldValue, int):

                newValue = int(newValue)

              elif isinstance(oldValue, float):

                newValue = float(newValue)

              elif isinstance(oldValue, bool):

                newValue = bool(newValue)

              elif isinstance(oldValue, list):

                newValue = [newValue]

                newValue.extend(oldValue[1:])

            except Exception, msg:

              print str(msg)

            if newValue != None:

              self.dataObject[propertyValueChangedEventArgs.ChangedItem.Label] = newValue

        sender.Refresh()

      def SetDataObject(self, dataObject, readOnly = True, categories={}):

        # Transform dictionary for quick access

        tmp = {}

        for categoryName, propNameList in categories.iteritems():

          for propertyName in propNameList:

            tmp[propertyName] = categoryName

        categories = tmp

        self.dataObject = dataObject

        properties = DynamicPropertyClass()

        if isinstance(self.dataObject, types.DictionaryType):

          for key, value in self.dataObject.iteritems():

            prop = CustomProperty(key, value, readOnly, True)

            if hasattr(value, "Category"):

              prop.Category = str(getattr(value, "Category"))

            elif categories.has_key(key):

              prop.Category = str(categories[key])

            if isinstance(value, types.ListType):

              if len(value) > 0:

                prop.Value = value[0]

              prop.IsList = True;

              for item in value[1:]:

                prop.AddListItem(str(item))

            properties.Add(prop)

        else:

          pass

        self.propertyGrid.SelectedObject = properties

    class Settings(object):

      def __init__(self):

        self.FileName  = "tordivel_colour_RGB_sidestilt.gif"

        self.Alignment = "CenterImage"

        self.BackColor = CLR.System.Drawing.SystemColors.Control

      def GetAlignmentEnum(self):

        return CLR.System.Enum.Parse(CLR.System.Windows.Forms.PictureBoxSizeMode, self.Alignment)

      def GetDataObject(self):

        return { "FileName"  : self.FileName,

                 "Alignment" : [self.Alignment, "AutoSize", "CenterImage", "Normal", "StretchImage", "Zoom" ],

                 "BackColor" : self.BackColor }

      def UpdateDataObject(self, dataObject):

        self.FileName  = dataObject["FileName"]

        self.Alignment = dataObject["Alignment"][0]

        self.BackColor = dataObject["BackColor"]

      def GetCategories(self):

        return { "Image"     : [ "FileName", "Alignment", "BackColor" ] }

    def __init__(self,name):

      self.name=name

      self.settings = LogoPanel.Settings()

      self.Dock = WinForms.DockStyle.Fill

      self.DockPadding.All = 0

      self.profilePath = GetStringValue("System.Profile")

      # Create picture box

      self._pictureBox = WinForms.PictureBox()

      self._pictureBox.Location  = CLR.System.Drawing.Point(0, 0)

      self._pictureBox.Name      = "pictureBox"

      self._pictureBox.Dock      = WinForms.DockStyle.Fill

      self.Controls.Add(self._pictureBox)

      self._load()

      # Create context menu

      self.components = CLR.System.ComponentModel.Container()

      self.contextMenuStrip = WinForms.ContextMenuStrip(self.components)

      self.settingsToolStripMenuItem = WinForms.ToolStripMenuItem()

      # contextMenuStrip

      self.contextMenuStrip.Items.Add(self.settingsToolStripMenuItem)

      self.contextMenuStrip.Name = "contextMenuStrip"

      self.contextMenuStrip.Size = CLR.System.Drawing.Size(153, 48)

      self.contextMenuStrip.Opening += CLR.System.ComponentModel.CancelEventHandler(self.contextMenuStrip_Opening)

      # settingsToolStripMenuItem

      self.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem"

      self.settingsToolStripMenuItem.Size = CLR.System.Drawing.Size(152, 22)

      self.settingsToolStripMenuItem.Text = "Settings"

      self.settingsToolStripMenuItem.Click += self.settingsToolStripMenuItem_Click

      self.ContextMenuStrip = self.contextMenuStrip;

    def _loadImage(self, path, folder, imageName):

      fullPath = os.path.join(path, folder, imageName)

      altPath = os.path.join(path, imageName)

      if os.path.isfile(fullPath):

          return CLR.System.Drawing.Image.FromFile(fullPath)

      elif os.path.isfile(altPath):

          return CLR.System.Drawing.Image.FromFile(altPath)

      elif os.path.isfile(imageName):

          return CLR.System.Drawing.Image.FromFile(imageName)

    def _load(self):

      self._pictureBox.Image     = self._loadImage(self.profilePath, "Python", self.settings.FileName)

      self._pictureBox.SizeMode  = self.settings.GetAlignmentEnum()

      self._pictureBox.BackColor = self.settings.BackColor

    def contextMenuStrip_Opening(self, sender, args):

      if not GetBoolValue("System.Service"):

        args.Cancel = True

    def settingsToolStripMenuItem_Click(self, sender, args):

      self.configure()

    def __str__(self):

      '''

      return unique plugin instance name

      '''

      return '%s_%s'%(self.__class__.__name__,self.name)

    def getConfig(self):

      '''

      return plugin configuration as string

      '''

      import SPB

      spb=SPB.CreateSpb()

      spb.setText('Filename',self.settings.FileName)

      spb.setText('Alignment',self.settings.Alignment)

      spb.setInt('BackColor',self.settings.BackColor.ToArgb())

      return spb.xml

    def setConfig(self,value):

      '''

      set plugin configuration from string

      '''

      import SPB

      spb=SPB.CreateSpb(value)

      try:

        if spb.isEntry('Filename'):self.settings.FileName=spb.getText('Filename')

        if spb.isEntry('Alignment'):self.settings.Alignment=spb.getText('Alignment')

        if spb.isEntry('BackColor'):self.settings.BackColor=self.settings.BackColor.FromArgb(spb.getInt('BackColor'))

        self._load()

      except:

        print self,': invalid configuration'

    def configure(self):

      '''

      launch the plugin configuration dialog

      '''

      dlg = LogoPanel.PropertiesDialog()

      dlg.SetDataObject(self.settings.GetDataObject(), False, self.settings.GetCategories())

      if dlg.ShowDialog() == WinForms.DialogResult.OK:

        self.settings.UpdateDataObject(dlg.dataObject)

        self._load();

        PluginChanged(self)     #notify host changed

  backPanel = dotNetUtils.GetDotNetPanel(hWnd)

  panel = LogoPanel(name)

  panel.Parent = backPanel

  return panel