Follow along with this tutorial to write your own extension that can transfer data back and forth between Microsoft Excel and NVIDIA Omniverse. This tutorial not only demonstrates how to connect Excel and Omniverse specifically, but also serves as a template you can use to connect to other COM applications such as other Microsoft Office Applications as well as CATIA V5. It can also serve as a starting point to connect to applications that have Python, .NET or other libraries available.
- Use COM libraries from an Omniverse extension
- Listen for and respond to changes in Excel
- Update an Omniverse Scene
- Listen for and respond to changes in Omniverse
- Update an Excel spreadsheet
- Cloned Kit App Template
- Created a Kit Base Editor
Kit Applications are a combination of multiple, custome developed, extensions. In this section, you create a base extension from the extension template. Later you will modify this extension to connect Excel to an OpenUSD scene.
In VSCode open a new terminal window, if one is not already open.
In the terminal, run the template new
command.
Linux:
./repo.sh template new
Windows:
.\repo.bat template new
Follow the prompt instructions, Use the arrow keys to move and Enter to select/continue:
- ? Select with arrow keys what you want to create: Extension
- ? Select with arrow keys your desired template:: Python UI Extension
- ? Enter name of extension [name-spaced, lowercase, alphanumeric]:: my.spawn_prims.ext
- ? Enter extension_display_name:: Spawn Primitives
- ? Enter version:: 0.1.0
In VSCode, go to source > apps > [YOUR KIT BASE EDITOR APP].kit
, scroll to the [dependencies]
section.
At the bottom of the [dependencies]
section, add the following line:
"my.spawn_prims.ext" = {}
Save the .kit
file.
After a new extension has been added to the .kit file, the application should be rebuilt to ensure extensions are populated to the build directory.
In the terminal, run the build
command.
Linux:
./repo.sh build
Windows:
.\repo.bat build
In the terminal, run the launch
command.
Linux:
./repo.sh launch
Windows:
.\repo.bat launch
? Select with arrow keys which App would you like to launch: [Select the desired editor application]
NOTE: The initial startup may take 5 to 8 minutes as shaders compile for the first time. After initial shader compilation, startup time will reduce dramatically
Upon launching your Application, a new window appears in your Application, called "Spawn Primitives":
If you click Add in Spawn Primitives, the empty
text changes to count: 1
, indicating that the button was clicked. Pressing Add again increases the number for count. Pressing Reset will reset it back to empty
:
You use this button later to spawn a primitive.
DO NOT close out of the Application.
To get started, build a new extension from the extension template as demonstrated in this video and open the extension.py
file.
From there, replace the code in extension.py
with the code below:
import omni.ext
import omni.ui as ui
# Step 7.1
# Step 4.1
# Step 4.2
# Step 4.3
# Step 6.1
# Step 7.5
# Step 6.2
# Step 7.2
# Step 7.3
# Step 7.4
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
# Step 3.2
# Step 5
# Step 7.6
# Step 8.1
# Step 8.2
# Step 3.2
# Step 10
# Step 8.3
# Step 9.1
# Step 9.2
def on_shutdown(self):
# Step 10
pass
So far, the extension only contains comments with step numbers that correspond with the steps in this tutorial as well as the minimum code needed to launch an extension with a window. The OmniSampleExcel_connectionExtension
class encapsulates the extension itself. The on_startup
function runs when the extension is loaded. Currently, this function simply creates an empty window with Excel Connection
as its title.
This serves as a roadmap for the rest of the tutorial. At each step we will add code to the marked location. After each major step, such as Step 3
you can build and launch the extension to see your progress. After each minor step, such as Step 6.1
, the code may be missing necessary elements to run.
Note: Your class name will depend on the name you gave your extension upon creation and will be different from the class name,
OmniSampleExcel_connectionExtension
, in this tutorial.
Next we will build the user interface for the extension.
The first step is to add the following code below the assignment of self._window
in the on_startup
function:
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
Here is where visual parts of the user interface is built. You will see that there is a vertical stack which contains two horizontal stacks. This creates two rows in the user interface, and each row has two columns. The first row has a string field where the user can enter a path to the Excel spreadsheet they would like to connect to Omniverse. The second row has buttons that can connect or disconnect the given spreadsheet.
Add the following Code block to your extension's class after the on_startup
function:
def on_Connect_Click(self):
# Step 5
pass
def on_Disconnect_Click(self):
# Step 10
pass
Each ui.Button
has a clicked_fn
argument passed into its constructor. This is the function that will run when the button is clicked. For now, we have simply define the functions and have them do nothing, so that our UI is complete and the extension will build.
Saving the file at this point should launch the extension and let you see your progress. To view the full code up until this point, click to expand the item below.
Click to Reveal Code
import omni.ext
import omni.ui as ui
# Step 7.1
# Step 4.1
# Step 4.2
# Step 4.3
# Step 6.1
# Step 7.5
# Step 6.2
# Step 7.2
# Step 7.3
# Step 7.4
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\projects\kit-extension-sample-excel-connection\Assets\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
# Step 3.2
def on_Connect_Click(self):
# Step 5
# Step 7.6
# Step 8.1
# Step 8.2
pass
# Step 3.2
def on_Disconnect_Click(self):
# Step 10
pass
# Step 8.3
# Step 9.1
# Step 9.2
def on_shutdown(self):
# Step 10
pass
And so, with just a few lines of code the user interface for the Excel connector is complete. Next we will add the dependencies required to call the Excel API from within the extension.
Many applications have automation API libraries. These libraries come in a wide variety of languages and formats; this sample is helpful for any libraries that can be accessed from Python. If the libraries are Python libraries, which is common, this is very straightforward. While not quite as simple to import as Python libraries, .NET libraries can be brought in by using the Python.NET library
. COM libraries are a bit trickier to use, which is one reason they were chosen for this tutorial. Hopefully demonstrating how to import COM libraries will serve as a head start for those who want to use Python, .NET, COM or other technologies.
Add the following snippet to the top of your extension.py
file below the current imports:
import omni.kit.pipapi
omni.kit.pipapi.install("pywin32")
This code snippet installs pip packages that are not distributed with NVIDIA Omniverse, opening up a wide range of possibilities. In this case it installs pywin32
, a library that allows us to work with COM .dlls that are registered on the user's local machine such as Microsoft Office and CATIA V5.
Note: First-time pip installation of
pywin32
sometimes fails from within the Omniverse environment. If this happens copy the attempted pip command from the information-level console logs and run it in a command prompt.
Add this snippet to your code below the last snippet:
# you also need to set the following environment variables to install pywin32
import os
import sys
import carb
from pathlib import Path
import Pythonwin.pywin
dlls_path = Path(Pythonwin.pywin.__file__).parent.parent.parent / "pywin32_system32"
com = Path(Pythonwin.pywin.__file__).parent.parent.parent / "win32"
lib = Path(Pythonwin.pywin.__file__).parent.parent.parent / "win32" / "lib"
pywin = Path(Pythonwin.pywin.__file__).parent.parent.parent / "Pythonwin"
carb.log_info(dlls_path)
sys.path.insert(0, str(com))
sys.path.insert(0, str(lib))
sys.path.insert(0, str(pywin))
carb.log_info(sys.path)
os.environ["PATH"] = f"{dlls_path};{os.environ['PATH']}"
carb.log_info(os.environ["PATH"])
# End of pywin32 installation.
This step is less common when installing a package and probably will not be necessary unless you are working with COM APIs. In order to use the win32com.client
library which is required to access COM APIs, a few environment variables must be set within the Omniverse environment. If you were to use pywin32
in a local Python environment, you would first install the package and then run an installer that sets these environment variables. We cannot do this within the Omnviverse environment, so instead we install the library into the environment by hand. In the case that you are working with COM libraries and thus pywin32
, you can reuse this exact snippet.
Now that pywin32
is completely installed, the next step is to import win32com.client
which will give access to COM APIs. It is imported as follows:
# win32com.client lets you work with com libraries
import win32com.client
Saving the file at this point should launch the extension and let you see your progress. To view the full code up until this point, click to expand the item below.
Click to Reveal Code
import omni.ext
import omni.ui as ui
# Step 7.1
# Step 4.1
# In order to work with com you will need to import pywin32
import omni.kit.pipapi
omni.kit.pipapi.install("pywin32")
# Step 4.2
# you also need to set the following environment variables to install pywin32
import os
import sys
import carb
from pathlib import Path
import pythonwin.pywin
dlls_path = Path(pythonwin.pywin.__file__).parent.parent.parent / "pywin32_system32"
com = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32"
lib = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32" / "lib"
pywin = Path(pythonwin.pywin.__file__).parent.parent.parent / "pythonwin"
carb.log_info(dlls_path)
sys.path.insert(0, str(com))
sys.path.insert(0, str(lib))
sys.path.insert(0, str(pywin))
carb.log_info(sys.path)
os.environ["PATH"] = f"{dlls_path};{os.environ['PATH']}"
carb.log_info(os.environ["PATH"])
# End of pywin32 installation.
# Step 4.3
# win32com.client lets you work with com libraries
import win32com.client
# Step 6.1
# Step 7.5
# Step 6.2
# Step 7.2
# Step 7.3
# Step 7.4
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\projects\kit-extension-sample-excel-connection\Assets\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
# Step 3.2
def on_Connect_Click(self):
# Step 5
# Step 7.6
# Step 8.1
# Step 8.2
pass
# Step 3.2
def on_Disconnect_Click(self):
# Step 10
pass
# Step 8.3
# Step 9.1
# Step 9.2
def on_shutdown(self):
# Step 10
pass
With that, all libraries needed to work with COM APIs have been installed and imported. In the next step we will use this library to access the Excel application.
The sample connects to Excel in the on_Connect_Click
function using the following snippet:
# Link to Excel
self._excel_app = win32com.client.DispatchEx("excel.application")
self._excel_app.Visible = True
# Open workbook
self._excel_workbook = self._excel_app.Workbooks.Open(self._sheet_path.as_string)
try:
if hasattr(self._excel_workbook, 'Worksheets'):
self._excel_worksheet = self._excel_workbook.Worksheets(1)
else:
self._excel_worksheet = self._excel_workbook._dispobj_.Worksheets(1)
except:
carb.log_info("Could not find Worksheets attribute")
return
If you have spent time automating Excel with VBA or .NET, this code might be familiar to you. That is because this snippet is calling the same functions from the same libraries! First it launches the Excel application; then it makes sure it is visible. Next, this snippet opens the workbook specified in the user interface. In the next step where a variable is assigned the value of the first worksheet in the worksheets collection, there is an if
else
statement with slightly cryptic code. This statement checks if the worksheets property is where we would expect it to be; if not it looks for it under the _dispobj_
property. There appears to be a bug in win32com
that sometimes includes this extra layer in the class hierarchy and so the worksheets property can be found at either of these two paths.
NOTE: COM libraries do not typically have Python documentation. In order to learn how to automate these applications, it is recommended you use their VBA documentation and adapt it to Python. The Excel VBA documentation can be found here. It can also be helpful to prototype functionality in the VBA environment and then adapt it to Python. The VBA Object browser is another tool that can help you identify API elements which can be accessed from an application's COM libraries.
Saving the file at this point should launch the extension and let you see your progress. Clicking the Connect
button should open the Excel spreadsheet listed in the Spreadsheet Path
string field. To view the full code up until this point, click to expand the item below.
Click to Reveal Code
import omni.ext
import omni.ui as ui
# Step 7.1
# Step 4.1
# In order to work with com you will need to import pywin32
import omni.kit.pipapi
omni.kit.pipapi.install("pywin32")
# Step 4.2
# you also need to set the following environment variables to install pywin32
import os
import sys
import carb
from pathlib import Path
import pythonwin.pywin
dlls_path = Path(pythonwin.pywin.__file__).parent.parent.parent / "pywin32_system32"
com = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32"
lib = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32" / "lib"
pywin = Path(pythonwin.pywin.__file__).parent.parent.parent / "pythonwin"
carb.log_info(dlls_path)
sys.path.insert(0, str(com))
sys.path.insert(0, str(lib))
sys.path.insert(0, str(pywin))
carb.log_info(sys.path)
os.environ["PATH"] = f"{dlls_path};{os.environ['PATH']}"
carb.log_info(os.environ["PATH"])
# End of pywin32 installation.
# Step 4.3
# win32com.client lets you work with com libraries
import win32com.client
# Step 6.1
# Step 7.5
# Step 6.2
# Step 7.2
# Step 7.3
# Step 7.4
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\projects\kit-extension-sample-excel-connection\Assets\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
# Step 3.2
def on_Connect_Click(self):
# Step 5
# Link to Excel
self._excel_app = win32com.client.DispatchEx("excel.application")
self._excel_app.Visible = True
# Open workbook
self._excel_workbook = self._excel_app.Workbooks.Open(self._sheet_path.as_string)
try:
if hasattr(self._excel_workbook, 'Worksheets'):
self._excel_worksheet = self._excel_workbook.Worksheets(1)
else:
self._excel_worksheet = self._excel_workbook._dispobj_.Worksheets(1)
except:
carb.log_info("Could not find Worksheets attribute")
return
# Step 7.6
# Step 8.1
# Step 8.2
# Step 3.2
def on_Disconnect_Click(self):
# Step 10
pass
# Step 8.3
# Step 9.1
# Step 9.2
def on_shutdown(self):
# Step 10
pass
Now that the extension can open the Excel spreadsheet, the next section will explain how to subscribe to changes from Excel.
If you have done Excel automation with VBA, you have probably used the Worksheet_Change
function to perform actions whenever a user edits a worksheet. We do the same thing in this sample in four steps. First, identify the class and event name you would like to subscribe to. In the case of Excel we can use the VBA object browser to do this. For this sample, we want to listen for changes to a worksheet. We find the Worksheet
class and if you look there is a Change
event as shown in the image below:
Add the following class definition to extension.py
above your extension's class:
class WorksheetEvents:
Please note that the class has the same name as the class we want to subscribe to plus the Events
appended to it, in this case WorksheetEvents
. This is not required but is a recommended convention.
Next add the following function to the WorksheetEvents
class:
def OnChange(self, *args):
pass
It is necessary that this function has the same name as the event in the VBA object browser, prepended with On
. In this case the event in the VBA object browser was Change
and so the function name to create is OnChange
. If this function is not named correctly it will not work. The function will be an instance function, so it will have self
as an argument, as well as a pointer to any arguments expected from the COM library.
Saving the file at this point should launch the extension and let you see your progress. To view the full code up until this point, click to expand the item below.
Click to Reveal Code
import omni.ext
import omni.ui as ui
# Step 7.1
# Step 4.1
# In order to work with com you will need to import pywin32
import omni.kit.pipapi
omni.kit.pipapi.install("pywin32")
# Step 4.2
# you also need to set the following environment variables to install pywin32
import os
import sys
import carb
from pathlib import Path
import pythonwin.pywin
dlls_path = Path(pythonwin.pywin.__file__).parent.parent.parent / "pywin32_system32"
com = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32"
lib = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32" / "lib"
pywin = Path(pythonwin.pywin.__file__).parent.parent.parent / "pythonwin"
carb.log_info(dlls_path)
sys.path.insert(0, str(com))
sys.path.insert(0, str(lib))
sys.path.insert(0, str(pywin))
carb.log_info(sys.path)
os.environ["PATH"] = f"{dlls_path};{os.environ['PATH']}"
carb.log_info(os.environ["PATH"])
# End of pywin32 installation.
# Step 4.3
# win32com.client lets you work with com libraries
import win32com.client
# Step 6.1
# This class mirrors the events in the com dll you would like to subscribe to
class WorksheetEvents:
# Step 7.5
# Step 6.2
def OnChange(self, *args):
# Step 7.2
# Step 7.3
# Step 7.4
pass
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\projects\kit-extension-sample-excel-connection\Assets\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
# Step 3.2
def on_Connect_Click(self):
# Step 5
# Link to Excel
self._excel_app = win32com.client.DispatchEx("excel.application")
self._excel_app.Visible = True
# Open workbook
self._excel_workbook = self._excel_app.Workbooks.Open(self._sheet_path.as_string)
try:
if hasattr(self._excel_workbook, 'Worksheets'):
self._excel_worksheet = self._excel_workbook.Worksheets(1)
else:
self._excel_worksheet = self._excel_workbook._dispobj_.Worksheets(1)
except:
carb.log_info("Could not find Worksheets attribute")
return
# Step 7.6
# Step 8.1
# Step 8.2
# Step 3.2
def on_Disconnect_Click(self):
# Step 10
pass
# Step 8.3
# Step 9.1
# Step 9.2
def on_shutdown(self):
# Step 10
pass
First, add the following imports to the top of the extension.py
:
import omni.usd
import re
In this section we will be working with the Omniverse scene and so we need to use omni.usd
. We will use regular expressions to compare strings so we need to import re
.
with those libraries imported, we will now perform the following steps in the OnChange
function created in the last section:
- Check if the change happened in a cell associated with a tracked prim
- Get the prim that has changed
- Move the prim that has changed
The first step is to check whether the address of the changed cell is one we are tracking. Add the following snippet to your OnChange
function.
# check if changed cell is one we are tracking
try:
address_pattern = r'\$[DE]\$[3456]'
address = str(args[0].Address)
if not re.match(address_pattern, address):
return
except Exception as e:
carb.log_error('Could not detect cell changes' + e)
In this snippet we create a regular expression that will match cell addresses that contain tracked prims and then check whether the changed address matches that pattern. If it does not match we do not continue any further.
The following snippet gets the prim from the USD
scene and goes in your OnChange
function next:
# get prim path from Excel
prim_path_cell_address = r"C" + address[3]
prim_path = WorksheetEvents._excel_worksheet.Range(prim_path_cell_address).Value
stage = omni.usd.get_context().get_stage()
prim = stage.GetPrimAtPath(prim_path)
if not prim.IsValid():
carb.log_error("Can't find prim at path")
return
The second step is to find the prim path that has been effected by the change. This has been made easy in this sample by storing the prim's path in the spreadhseet to be retrieved later.
The row is taken from the event argment and appended to the column containing the prim path. Then, the value of that cell is read and that string is used within the Omniverse stage to retrieve the prim. It is important to check whether the prim is valid before continuing, because it is very easy to make a mistake when working with prim paths.
Finally, we will move the prim to its new location by adding this snippet to your OnChange
function:
# move prim to new coordinates
new_value = WorksheetEvents._excel_worksheet.Range(address).Value
translate = prim.GetAttribute("xformOp:translate").Get()
if (address[1] == "D"):
translate[0] = new_value
else:
translate[1] = new_value
prim.GetAttribute("xformOp:translate").Set(translate)
First, read the new position from Excel. Second read the current position from Omniverse. Third assign the new value from Excel to the Omniverse Translation vector and finally, write the new translation vector back to Omniverse.
Add this variable to the WorksheetEvents
class:
_excel_worksheet = None
This variable has been added so that the connected Excel spreadsheet can be accessed from within the event function.
Add the following two lines of code to the on_Connect_Click
function:
WorksheetEvents._excel_worksheet = self._excel_worksheet
self._excel_events = win32com.client.WithEvents(self._excel_worksheet, WorksheetEvents)
this sets the _excel_worksheet
variable and then subscribes to the worksheet's OnChanged
event with the help of the win32com.client
library.
This is a great point to pause and check your work. You should be able to launch the extension and if you change the position of a prim in Excel, the prim should move in Omniverse. Saving the file at this point should launch the extension and let you see your progress. To view the full code up until this point, click to expand the item below.
Click to Reveal Code
import omni.ext
import omni.ui as ui
# Step 7.1
import omni.usd
import re
# Step 4.1
# In order to work with com you will need to import pywin32
import omni.kit.pipapi
omni.kit.pipapi.install("pywin32")
# Step 4.2
# you also need to set the following environment variables to install pywin32
import os
import sys
import carb
from pathlib import Path
import pythonwin.pywin
dlls_path = Path(pythonwin.pywin.__file__).parent.parent.parent / "pywin32_system32"
com = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32"
lib = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32" / "lib"
pywin = Path(pythonwin.pywin.__file__).parent.parent.parent / "pythonwin"
carb.log_info(dlls_path)
sys.path.insert(0, str(com))
sys.path.insert(0, str(lib))
sys.path.insert(0, str(pywin))
carb.log_info(sys.path)
os.environ["PATH"] = f"{dlls_path};{os.environ['PATH']}"
carb.log_info(os.environ["PATH"])
# End of pywin32 installation.
# Step 4.3
# win32com.client lets you work with com libraries
import win32com.client
# Step 6.1
# This class mirrors the events in the com dll you would like to subscribe to
class WorksheetEvents:
# Step 7.5
_excel_worksheet = None
# Step 6.2
def OnChange(self, *args):
# Step 7.2
# check if changed cell is one we are tracking
try:
address_pattern = r'\$[DE]\$[3456]'
address = str(args[0].Address)
if not re.match(address_pattern, address):
return
except Exception as e:
carb.log_error('Could not detect cell changes' + e)
# Step 7.3
# get prim path from excel
prim_path_cell_address = r"C" + address[3]
prim_path = WorksheetEvents._excel_worksheet.Range(prim_path_cell_address).Value
stage = omni.usd.get_context().get_stage()
prim = stage.GetPrimAtPath(prim_path)
if not prim.IsValid():
carb.log_error("Can't find prim at path")
return
# Step 7.4
# move prim to new coordinates
new_value = WorksheetEvents._excel_worksheet.Range(address).Value
translate = prim.GetAttribute("xformOp:translate").Get()
if (address[1] == "D"):
translate[0] = new_value
else:
translate[1] = new_value
prim.GetAttribute("xformOp:translate").Set(translate)
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\projects\kit-extension-sample-excel-connection\Assets\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
# Step 3.2
def on_Connect_Click(self):
# Step 5
# Link to Excel
self._excel_app = win32com.client.DispatchEx("excel.application")
self._excel_app.Visible = True
# Open workbook
self._excel_workbook = self._excel_app.Workbooks.Open(self._sheet_path.as_string)
try:
if hasattr(self._excel_workbook, 'Worksheets'):
self._excel_worksheet = self._excel_workbook.Worksheets(1)
else:
self._excel_worksheet = self._excel_workbook._dispobj_.Worksheets(1)
except:
carb.log_info("Could not find Worksheets attribute")
return
# Step 7.6
WorksheetEvents._excel_worksheet = self._excel_worksheet
self._excel_events = win32com.client.WithEvents(self._excel_worksheet, WorksheetEvents)
# Step 8.1
# Step 8.2
# Step 3.2
def on_Disconnect_Click(self):
# Step 10
pass
# Step 8.3
# Step 9.1
# Step 9.2
def on_shutdown(self):
# Step 10
pass
In the next section we will subscribe to Omniverse changes so that we can get data flowing back in the other direction.
Working with Omniverse in Python is more straightforward than working with COM APIs in Python because Omniverse was designed to work with Python. In Omniverse we can deliberately subscribe to changes in a specific prim attribute as shown below:
self._stage = omni.usd.get_context().get_stage()
watcher = omni.usd.get_watcher()
self.prim_1 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C3').Value)
if self.prim_1.IsValid():
translate_attr = self.prim_1.GetAttribute("xformOp:translate")
self.watcher1 = watcher.subscribe_to_change_info_path(
translate_attr.GetPath(),
self._translate_changed
)
In this snippet, the first prim listed in the Excel spreadsheet is retrieved. If the prim is valid, its translate attribute is assigned to a variable and then a watcher is set that listens for any changes to that attribute.
This is then done for the other three pallets in the Excel spreadsheet:
self.prim_2 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C4').Value)
if self.prim_2.IsValid():
translate_attr = self.prim_2.GetAttribute("xformOp:translate")
self.watcher2 = watcher.subscribe_to_change_info_path(
translate_attr.GetPath(),
self._translate_changed
)
self.prim_3 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C5').Value)
if self.prim_3.IsValid():
translate_attr = self.prim_3.GetAttribute("xformOp:translate")
self.watcher3 = watcher.subscribe_to_change_info_path(
translate_attr.GetPath(),
self._translate_changed
)
self.prim_4 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C6').Value)
if self.prim_4.IsValid():
translate_attr = self.prim_4.GetAttribute("xformOp:translate")
self.watcher4 = watcher.subscribe_to_change_info_path(
translate_attr.GetPath(),
self._translate_changed
)
Don't forget to create the _translate_changed
function which is called when one of these prims is changed:
def _translate_changed(self, *args):
pass
Saving the file at this point should launch the extension and let you see your progress. To view the full code up until this point, click to expand the item below.
Click to Reveal Code
import omni.ext
import omni.ui as ui
# Step 7.1
import omni.usd
import re
# Step 4.1
# In order to work with com you will need to import pywin32
import omni.kit.pipapi
omni.kit.pipapi.install("pywin32")
# Step 4.2
# you also need to set the following environment variables to install pywin32
import os
import sys
import carb
from pathlib import Path
import pythonwin.pywin
dlls_path = Path(pythonwin.pywin.__file__).parent.parent.parent / "pywin32_system32"
com = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32"
lib = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32" / "lib"
pywin = Path(pythonwin.pywin.__file__).parent.parent.parent / "pythonwin"
carb.log_info(dlls_path)
sys.path.insert(0, str(com))
sys.path.insert(0, str(lib))
sys.path.insert(0, str(pywin))
carb.log_info(sys.path)
os.environ["PATH"] = f"{dlls_path};{os.environ['PATH']}"
carb.log_info(os.environ["PATH"])
# End of pywin32 installation.
# Step 4.3
# win32com.client lets you work with com libraries
import win32com.client
# Step 6.1
# This class mirrors the events in the com dll you would like to subscribe to
class WorksheetEvents:
# Step 7.5
_excel_worksheet = None
# Step 6.2
def OnChange(self, *args):
# Step 7.2
# check if changed cell is one we are tracking
try:
address_pattern = r'\$[DE]\$[3456]'
address = str(args[0].Address)
if not re.match(address_pattern, address):
return
except Exception as e:
carb.log_error('Could not detect cell changes' + e)
# Step 7.3
# get prim path from excel
prim_path_cell_address = r"C" + address[3]
prim_path = WorksheetEvents._excel_worksheet.Range(prim_path_cell_address).Value
stage = omni.usd.get_context().get_stage()
prim = stage.GetPrimAtPath(prim_path)
if not prim.IsValid():
carb.log_error("Can't find prim at path")
return
# Step 7.4
# move prim to new coordinates
new_value = WorksheetEvents._excel_worksheet.Range(address).Value
translate = prim.GetAttribute("xformOp:translate").Get()
if (address[1] == "D"):
translate[0] = new_value
else:
translate[1] = new_value
prim.GetAttribute("xformOp:translate").Set(translate)
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\projects\kit-extension-sample-excel-connection\Assets\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
# Step 3.2
def on_Connect_Click(self):
# Step 5
# Link to Excel
self._excel_app = win32com.client.DispatchEx("excel.application")
self._excel_app.Visible = True
# Open workbook
self._excel_workbook = self._excel_app.Workbooks.Open(self._sheet_path.as_string)
try:
if hasattr(self._excel_workbook, 'Worksheets'):
self._excel_worksheet = self._excel_workbook.Worksheets(1)
else:
self._excel_worksheet = self._excel_workbook._dispobj_.Worksheets(1)
except:
carb.log_info("Could not find Worksheets attribute")
return
# Step 7.6
WorksheetEvents._excel_worksheet = self._excel_worksheet
self._excel_events = win32com.client.WithEvents(self._excel_worksheet, WorksheetEvents)
# Step 8.1
# Link to Scene
self._stage = omni.usd.get_context().get_stage()
watcher = omni.usd.get_watcher()
self.prim_1 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C3').Value)
if self.prim_1.IsValid():
translate_attr = self.prim_1.GetAttribute("xformOp:translate")
self.watcher1 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
# Step 8.2
self.prim_2 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C4').Value)
if self.prim_2.IsValid():
translate_attr = self.prim_2.GetAttribute("xformOp:translate")
self.watcher2 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
self.prim_3 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C5').Value)
if self.prim_3.IsValid():
translate_attr = self.prim_3.GetAttribute("xformOp:translate")
self.watcher3 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
self.prim_4 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C6').Value)
if self.prim_4.IsValid():
translate_attr = self.prim_4.GetAttribute("xformOp:translate")
self.watcher4 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
# Step 3.2
def on_Disconnect_Click(self):
# Step 10
pass
# Step 8.3
def _translate_changed(self, *args):
# Step 9.1
# Step 9.2
pass
def on_shutdown(self):
# Step 10
pass
With the subscription made, the next step in the tutorial is to respond to changes that come from Omniverse and update Excel.
Responding to changes from Omniverse follows two steps:
- Check if the translation value has changed
- Update the values in Excel
Add this snippet to the _translate_changed
function:
# Check if the translation in Excel is different
translate_attribute = self._stage.GetAttributeAtPath(args[0])
translate = translate_attribute.Get()
prim_path = translate_attribute.GetPrimPath()
next_address = ""
row = 3
found = False
for row in range(3, 7):
next_address = "C" + str(row)
next_path = self._excel_worksheet.Range(next_address).Value
if next_path == prim_path:
found = True
break
if not found:
carb.log_info("prim not found in Excel worksheet")
return
x_address = "D" + str(row)
excel_x = self._excel_worksheet.Range(x_address).Value
y_address = "E" + str(row)
excel_y = self._excel_worksheet.Range(y_address).Value
# No change in value
if excel_x == translate[0] and excel_y == translate[1]:
return
This code snippet first gets the translate value as well as the prim path from the attribute path passed into the event. Next, Excel is searched to find the row that matches the prim path. Once the correct row is found, the X and Y values are read from Excel. Finally, these values are compared with the current Omniverse values and if they have note changed the event handler returns.
Note: It is critical with any bi-directional connection such as this that at some point you check whether values have actually changed. If you do not do this, the connector will enter an infinite loop.
If either of the values is different, the code continues with the following snippet:
# If so change it.
self._excel_worksheet.Range(x_address).Value = translate[0]
self._excel_worksheet.Range(y_address).Value = translate[1]
This uses Excel's COM API to change the cell values of interest.
Saving the file at this point should launch the extension and let you see your progress. To view the full code up until this point, click to expand the item below.
Click to Reveal Code
import omni.ext
import omni.ui as ui
# Step 7.1
import omni.usd
import re
# Step 4.1
# In order to work with com you will need to import pywin32
import omni.kit.pipapi
omni.kit.pipapi.install("pywin32")
# Step 4.2
# you also need to set the following environment variables to install pywin32
import os
import sys
import carb
from pathlib import Path
import pythonwin.pywin
dlls_path = Path(pythonwin.pywin.__file__).parent.parent.parent / "pywin32_system32"
com = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32"
lib = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32" / "lib"
pywin = Path(pythonwin.pywin.__file__).parent.parent.parent / "pythonwin"
carb.log_info(dlls_path)
sys.path.insert(0, str(com))
sys.path.insert(0, str(lib))
sys.path.insert(0, str(pywin))
carb.log_info(sys.path)
os.environ["PATH"] = f"{dlls_path};{os.environ['PATH']}"
carb.log_info(os.environ["PATH"])
# End of pywin32 installation.
# Step 4.3
# win32com.client lets you work with com libraries
import win32com.client
# Step 6.1
# This class mirrors the events in the com dll you would like to subscribe to
class WorksheetEvents:
# Step 7.5
_excel_worksheet = None
# Step 6.2
def OnChange(self, *args):
# Step 7.2
# check if changed cell is one we are tracking
try:
address_pattern = r'\$[DE]\$[3456]'
address = str(args[0].Address)
if not re.match(address_pattern, address):
return
except Exception as e:
carb.log_error('Could not detect cell changes' + e)
# Step 7.3
# get prim path from excel
prim_path_cell_address = r"C" + address[3]
prim_path = WorksheetEvents._excel_worksheet.Range(prim_path_cell_address).Value
stage = omni.usd.get_context().get_stage()
prim = stage.GetPrimAtPath(prim_path)
if not prim.IsValid():
carb.log_error("Can't find prim at path")
return
# Step 7.4
# move prim to new coordinates
new_value = WorksheetEvents._excel_worksheet.Range(address).Value
translate = prim.GetAttribute("xformOp:translate").Get()
if (address[1] == "D"):
translate[0] = new_value
else:
translate[1] = new_value
prim.GetAttribute("xformOp:translate").Set(translate)
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\projects\kit-extension-sample-excel-connection\Assets\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
# Step 3.2
def on_Connect_Click(self):
# Step 5
# Link to Excel
self._excel_app = win32com.client.DispatchEx("excel.application")
self._excel_app.Visible = True
# Open workbook
self._excel_workbook = self._excel_app.Workbooks.Open(self._sheet_path.as_string)
try:
if hasattr(self._excel_workbook, 'Worksheets'):
self._excel_worksheet = self._excel_workbook.Worksheets(1)
else:
self._excel_worksheet = self._excel_workbook._dispobj_.Worksheets(1)
except:
carb.log_info("Could not find Worksheets attribute")
return
# Step 7.6
WorksheetEvents._excel_worksheet = self._excel_worksheet
self._excel_events = win32com.client.WithEvents(self._excel_worksheet, WorksheetEvents)
# Step 8.1
# Link to Scene
self._stage = omni.usd.get_context().get_stage()
watcher = omni.usd.get_watcher()
self.prim_1 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C3').Value)
if self.prim_1.IsValid():
translate_attr = self.prim_1.GetAttribute("xformOp:translate")
self.watcher1 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
# Step 8.2
self.prim_2 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C4').Value)
if self.prim_2.IsValid():
translate_attr = self.prim_2.GetAttribute("xformOp:translate")
self.watcher2 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
self.prim_3 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C5').Value)
if self.prim_3.IsValid():
translate_attr = self.prim_3.GetAttribute("xformOp:translate")
self.watcher3 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
self.prim_4 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C6').Value)
if self.prim_4.IsValid():
translate_attr = self.prim_4.GetAttribute("xformOp:translate")
self.watcher4 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
# Step 3.2
def on_Disconnect_Click(self):
# Step 10
pass
# Step 8.3
def _translate_changed(self, *args):
# Step 9.1
# Check if the translation in excel is different
translate_attribute = self._stage.GetAttributeAtPath(args[0])
translate = translate_attribute.Get()
prim_path = translate_attribute.GetPrimPath()
next_address = ""
row = 3
found = False
for row in range(3, 7):
next_address = "C" + str(row)
next_path = self._excel_worksheet.Range(next_address).Value
if next_path == prim_path:
found = True
break
if not found:
carb.log_info("prim not found in excel worksheet")
return
x_address = "D" + str(row)
excel_x = self._excel_worksheet.Range(x_address).Value
y_address = "E" + str(row)
excel_y = self._excel_worksheet.Range(y_address).Value
# No change in value
if excel_x == translate[0] and excel_y == translate[1]:
return
# Step 9.2
# If so change it.
self._excel_worksheet.Range(x_address).Value = translate[0]
self._excel_worksheet.Range(y_address).Value = translate[1]
def on_shutdown(self):
# Step 10
pass
Finally, add this code to both the on_Disconnect_Click
function and the on_shutdown
function:
self._excel_events = None
self._excel_worksheet = None
if hasattr(self, '_excel_workbook'):
if self._excel_workbook is not None:
self._excel_workbook.Close(False)
self._excel_workbook = None
if hasattr(self, '_excel_app'):
if self._excel_app is not None:
self._excel_app.Application.Quit()
self._excel_app = None
The extension must gracefully disconnect from excel, both when the users clicks the disconnect
button and when the extension shuts down. This snippet releases all COM resources in reverse order that they were created.
Saving the file at this point should launch the extension and let you use the completed extension. To view the full sample, click to expand the item below.
Click to Reveal Code
import omni.ext
import omni.ui as ui
# Step 7.1
import omni.usd
import re
# Step 4.1
# In order to work with com you will need to import pywin32
import omni.kit.pipapi
omni.kit.pipapi.install("pywin32")
# Step 4.2
# you also need to set the following environment variables to install pywin32
import os
import sys
import carb
from pathlib import Path
import pythonwin.pywin
dlls_path = Path(pythonwin.pywin.__file__).parent.parent.parent / "pywin32_system32"
com = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32"
lib = Path(pythonwin.pywin.__file__).parent.parent.parent / "win32" / "lib"
pywin = Path(pythonwin.pywin.__file__).parent.parent.parent / "pythonwin"
carb.log_info(dlls_path)
sys.path.insert(0, str(com))
sys.path.insert(0, str(lib))
sys.path.insert(0, str(pywin))
carb.log_info(sys.path)
os.environ["PATH"] = f"{dlls_path};{os.environ['PATH']}"
carb.log_info(os.environ["PATH"])
# End of pywin32 installation.
# Step 4.3
# win32com.client lets you work with com libraries
import win32com.client
# Step 6.1
# This class mirrors the events in the com dll you would like to subscribe to
class WorksheetEvents:
# Step 7.5
_excel_worksheet = None
# Step 6.2
def OnChange(self, *args):
# Step 7.2
# check if changed cell is one we are tracking
try:
address_pattern = r'\$[DE]\$[3456]'
address = str(args[0].Address)
if not re.match(address_pattern, address):
return
except Exception as e:
carb.log_error('Could not detect cell changes' + e)
# Step 7.3
# get prim path from excel
prim_path_cell_address = r"C" + address[3]
prim_path = WorksheetEvents._excel_worksheet.Range(prim_path_cell_address).Value
stage = omni.usd.get_context().get_stage()
prim = stage.GetPrimAtPath(prim_path)
if not prim.IsValid():
carb.log_error("Can't find prim at path")
return
# Step 7.4
# move prim to new coordinates
new_value = WorksheetEvents._excel_worksheet.Range(address).Value
translate = prim.GetAttribute("xformOp:translate").Get()
if (address[1] == "D"):
translate[0] = new_value
else:
translate[1] = new_value
prim.GetAttribute("xformOp:translate").Set(translate)
class OmniSampleExcel_connectionExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[omni.sample.excel_connection] omni sample excel_connection startup")
self._window = ui.Window("Excel Connection", width=600, height=200)
# Step 3.1
with self._window.frame:
with ui.VStack():
self._sheet_path = ui.SimpleStringModel(r"C:\projects\kit-extension-sample-excel-connection\Assets\Warehouse_BOM.xlsx")
with ui.HStack(style={"margin": 5}, height=40):
ui.Label("Spreadsheet Path:", width=50)
ui.StringField(self._sheet_path, width=500)
with ui.HStack(style={"margin": 5}, height=40):
ui.Spacer()
ui.Button("Connect", clicked_fn=self.on_Connect_Click, width=300)
ui.Button("Disconnect", clicked_fn=self.on_Disconnect_Click, width=300)
ui.Spacer()
# Step 3.2
def on_Connect_Click(self):
# Step 5
# Link to Excel
self._excel_app = win32com.client.DispatchEx("excel.application")
self._excel_app.Visible = True
# Open workbook
self._excel_workbook = self._excel_app.Workbooks.Open(self._sheet_path.as_string)
try:
if hasattr(self._excel_workbook, 'Worksheets'):
self._excel_worksheet = self._excel_workbook.Worksheets(1)
else:
self._excel_worksheet = self._excel_workbook._dispobj_.Worksheets(1)
except:
carb.log_info("Could not find Worksheets attribute")
return
# Step 7.6
WorksheetEvents._excel_worksheet = self._excel_worksheet
self._excel_events = win32com.client.WithEvents(self._excel_worksheet, WorksheetEvents)
# Step 8.1
# Link to Scene
self._stage = omni.usd.get_context().get_stage()
watcher = omni.usd.get_watcher()
self.prim_1 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C3').Value)
if self.prim_1.IsValid():
translate_attr = self.prim_1.GetAttribute("xformOp:translate")
self.watcher1 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
# Step 8.2
self.prim_2 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C4').Value)
if self.prim_2.IsValid():
translate_attr = self.prim_2.GetAttribute("xformOp:translate")
self.watcher2 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
self.prim_3 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C5').Value)
if self.prim_3.IsValid():
translate_attr = self.prim_3.GetAttribute("xformOp:translate")
self.watcher3 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
self.prim_4 = self._stage.GetPrimAtPath(self._excel_worksheet.Range('C6').Value)
if self.prim_4.IsValid():
translate_attr = self.prim_4.GetAttribute("xformOp:translate")
self.watcher4 = watcher.subscribe_to_change_info_path(translate_attr.GetPath(), self._translate_changed)
# Step 3.2
def on_Disconnect_Click(self):
# Step 10
self._excel_events = None
self._excel_worksheet = None
if hasattr(self, '_excel_workbook'):
if self._excel_workbook is not None:
self._excel_workbook.Close(False)
self._excel_workbook = None
if hasattr(self, '_excel_app'):
if self._excel_app is not None:
self._excel_app.Application.Quit()
self._excel_app = None
# Step 8.3
def _translate_changed(self, *args):
# Step 9.1
# Check if the translation in excel is different
translate_attribute = self._stage.GetAttributeAtPath(args[0])
translate = translate_attribute.Get()
prim_path = translate_attribute.GetPrimPath()
next_address = ""
row = 3
found = False
for row in range(3, 7):
next_address = "C" + str(row)
next_path = self._excel_worksheet.Range(next_address).Value
if next_path == prim_path:
found = True
break
if not found:
carb.log_info("prim not found in excel worksheet")
return
x_address = "D" + str(row)
excel_x = self._excel_worksheet.Range(x_address).Value
y_address = "E" + str(row)
excel_y = self._excel_worksheet.Range(y_address).Value
# No change in value
if excel_x == translate[0] and excel_y == translate[1]:
return
# Step 9.2
# If so change it.
self._excel_worksheet.Range(x_address).Value = translate[0]
self._excel_worksheet.Range(y_address).Value = translate[1]
def on_shutdown(self):
# Step 10
self._excel_events = None
self._excel_worksheet = None
if hasattr(self, '_excel_workbook'):
if self._excel_workbook is not None:
self._excel_workbook.Close(False)
self._excel_workbook = None
if hasattr(self, '_excel_app'):
if self._excel_app is not None:
self._excel_app.Application.Quit()
self._excel_app = None
This sample and tutorial demonstrates how to connect an Excel spreadsheet to NVIDIA Omniverse through a Python extension. It has done this through COM libraries, but the general approach can be applied to many apps and technologies.