Friday, 2 December 2016

python - ArcMap/ArcObjects - Drawing graphics onscreen: Explanation or tutorial for Draw/Refresh/PartialRefresh methods?


I find myself woefully lacking in understanding of when, why, and how (which type) to do screen draw/refresh/partial refresh. For instance, in a standalone Python script outside the application boundary, I have a function that is trying to draw graphic elements in the data view. Specifically, the elements will be a TextElement on top of a RectangleElement. The function loads the two elements into a GroupElement, which is in turn added to the ActiveView's GraphicsContainer for drawing on the screen. (As far as I can tell the graphic elements are valid.) I am really not understanding what Draw/Refresh/ParitalRefresh methods to put where. If anyone can give/point to a good explanation or tutorial about screen drawing/refreshing I would appreciate it. Below is a bit of selected code from my function. I have a PartialRefresh in the last line, but I don't know if that is correct or not.


pGroupElement = NewObj(esriCarto.GroupElement, esriCarto.IGroupElement3)
pTextElement = NewObj(esriCarto.TextElement, esriCarto.ITextElement)
pRectElement = NewObj(esriCarto.RectangleElement, esriCarto.IElement)
.
.
(configure TextElement) [tedious code removed]
.
.

(configure RectElement)
.
.
ptxtElem = CType(pTextElement, esriCarto.IElement)

pGroupElement.AddElement(ptxtElem)
pGroupElement.AddElement(pRectElement)

pContainer = mxDoc.ActiveView.GraphicsContainer


pGrpElem = CType(pGroupElement, esriCarto.IElement)

pContainer.AddElement(pGrpElem, 0)

mxDoc.ActiveView.PartialRefresh(esriCarto.esriViewGraphics, None, None)

Added: So, is creating objects with the following code - as I do now - not creating them in process?


def NewObj(MyClass, MyInterface):
"""Creates a new comtypes POINTER object where\n\
MyClass is the class to be instantiated,\n\

MyInterface is the interface to be assigned"""
from comtypes.client import CreateObject
try:
ptr = CreateObject(MyClass, interface=MyInterface)
return ptr
except:
return None

If not, I gather I must use IObjectFactory. Assuming that my current objects are not in process, I must say I haven't seen any great performance hit, but I guess I'm just asking for trouble forcing lots of interprocess calls.


Thank you, all, for the help. I shall experiment when next I get the chance and let you know the results.




Answer



So, in the light of the tips in Kirk's link (ArcMap Automation: Man the Message Pumps!) and the knowledge that IObjectFactory must be used your scenario, here's my take on Python implementation of the discussed workaround.


To reiterate, the problem is that RPC calls, coming from a different process (python) might get stalled in ArcMap. The general idea described in the aforementioned article is that by pummeling ArcMap's message loop with WM_NULL messages, you force it to process your RPC calls as well.


I am not versed in Python, in fact this might only be my fifth piece of Python code or so, if memory serves. If anyone wants to improve/fix it in any way, go ahead. I ran it on Python 2.6.5.


import ctypes
import threading

class EsriAppMessageHelper(threading.Thread):
def __init__(self, windowHandle):
threading.Thread.__init__(self)

self.running = True
self.windowHandle = windowHandle

def stop(self):
self.running = False

def run(self):
while self.running:
ctypes.windll.user32.SendMessageA(self.windowHandle, 0, 0, 0)
#print 'WM_NULL to window %d sent' % self.windowHandle


def __enter__(self):
self.start()

def __exit__(self, type, value, traceback):
self.stop()

Basically, it's a thread which sends WM_NULL (0) messages to the given window. The __enter__ and __exit__ functions support Python's with statement. Here's how it's used (the ArcObjects related functions are taken from this post in a different question):


# Get the ArcMap's IApplication reference.
# We're in a different process than ArcMap's, so it gets resolved using IAppROT.

app = GetApp()

# WM_NULL messages will be sent from another thread while
# this block is executing
with EsriAppMessageHelper(app.hWnd):

objectFactory = CType(app, esriFramework.IObjectFactory) # use IObjectFactory
mxDoc = CType(app.Document, esriArcMapUI.IMxDocument)
graphicsContainer = mxDoc.ActiveView.GraphicsContainer


textElement = CType(objectFactory.Create("esriCarto.TextElement"), esriCarto.ITextElement)

# initialize the symbols and elements, assign their geometries
# etc...

graphicsContainer.AddElement(element, 0)
mxDoc.ActiveView.PartialRefresh(esriCarto.esriViewGraphics, None, None)

# At this point, we're out of the WITH block.
# The separate thread is stopped and no more WM_NULL messages are being sent.

print "DONE"

Try it out. I could not fully test it with my ArcMap 10 setup because I did not have any issues with the active view refresh before (it executed immediately and refresh was successful), but the WM_NULL messages do get sent.


No comments:

Post a Comment

arcpy - Changing output name when exporting data driven pages to JPG?

Is there a way to save the output JPG, changing the output file name to the page name, instead of page number? I mean changing the script fo...