This example demonstrates several aspects of MicroStation Python …
The UI is created using PyQt standard Widgets.
The QComboBox
shows a list of cells in the attached cell library.
The list is created using helper classes in the cell_library.py
module.
The placement interval value is shown in a QLineEdit widget. I've added a QIntValidator to constrain the value that a user may enter.
The cell scale value is shown in a QLineEdit widget. I've added a QDoubleValidator to constrain the value that a user may enter.
There is an unfortunate clash between the messaging mechanism used by MicroStation and that used by PyQt. The work-around is discussed below.
DgnElementSetTool
cmdPickLineElement
inherits from (i.e. is a sub-class of) DgnElementSetTool
.
The _OnPostLocate
gets an ElementHandle
from the supplied HitPath
and converts it to a CurveVector
…
eh = ElementHandle(path.GetHeadElem(), path.GetRoot()) curve = ICurvePathQuery.ElementToCurveVector(eh)
It tests the CurveVector
to verify that is is an open path (curve.IsOpenPath()
).
It accepts the CurveVector
if it contains at least one suitable type …
_ACCEPTABLE_TYPES = (ICurvePrimitive.eCURVE_PRIMITIVE_TYPE_Line,
ICurvePrimitive.eCURVE_PRIMITIVE_TYPE_LineString,
ICurvePrimitive.eCURVE_PRIMITIVE_TYPE_Arc,
ICurvePrimitive.eCURVE_PRIMITIVE_TYPE_CurveVector,
)
primitiveType = primitive.GetCurvePrimitiveType()
if primitiveType in acceptable_types:
# We can use this element
CurveLocationDetail
Cell placement involves calculating a set of points that lie on the chosen linear element.
At each point we extract the tangent to the curve.
Class CurveLocationDetail
was designed for exactly this purpose.
The logic for this part of the application is mostly copied from the AI-generated code you can read here.
Calculating the tangent is more complex than it need be, because we have to use some
other classes (DRay3d
and DVec3d
) on the way.
I've wrapped those steps in function get_tangent_at_point
…
def get_tangent_at_point(detail)->RotMatrix: ''' Compute the tangent at a point on a line and return its orientation as a rotation matrix. ''' vr = detail.PointAndUnitTangent() ray = vr.Value() direction = ray.direction x_axis = DVec3d.From (1.0, 0.0, 0.0) angle = direction.AngleToXY(x_axis) _Z_AXIS = 2 rotation = RotMatrix.FromAxisAndRotationAngle(_Z_AXIS, angle) # At this point we have the tangent orientation rotation.Invert() # Invert the matrix to rotate the cell to match the tangent return rotation
Then we place a cell, using that tangent to apply a rotation. Cell name and cell scale are obtained from the UI.
Both PyQt and TkInter use windows messaging. MicroStation uses Windows messaging. Unfortunately, the two messaging mechanisms clash, or more accurately become entangled in a deadlock.
A consequence of such a deadlock is that the host application (MicroStation) can freeze. Worse still, the application may crash (terminate with no explanation).
During development I found that placing any MicroStation function call in a QWidget
's action implementation causes a crash.
For example, this function crashes MicroStation …
def queue_action_pick_line(self):
# The following statement crashes MicroStation
cmdPickLineElement.InstallNewInstance() # Calls the DgnElementSetTool
constructor
My work-around removes code that calls into MicroStation from those PyQt action functions. It re-implements the calls outside the PyQt messaging loop by setting an action flag …
def queue_action_pick_line(self): self._cmd_pick_line_requested = True
The action flags are tested in the UI's messaging loop …
def ms_mainLoop(self): """ Custom main loop that pumps both Qt and MicroStation events, and executes any deferred tool-start calls in a safe context. This loop is the key to letting the UI and tool coexist. """ while win32gui.IsWindow(self._storedWinId): # Standard pump for a hybrid Qt/MicroStation application self._loop.processEvents() PyCadInputQueue.PythonMainLoop() self._set_requested_flags() # Check if a tool start was requested by a QWidget action ((mod, cmd), flag) = self._requests.have_request() if flag: # If yes, run the stored function (e.g., InstallNewInstance) print(f"requested '{mod}:{cmd}'") invoke_function(mod, cmd, self) # Clear the flag so it only runs once per click self._requests.clear_requests() time.sleep(0.001)
That _requests logic is encapsulated in class CommandRequests
(module command_requests.py
).
That logic is clunky and not scaleable. If this application had, say, ten commands and not two then maintenance becomes harder. A permanent solution would be if Bentley Systems could create a better way to raise a barrier between MicroStation messaging and UI messaging.
Unpack the ZIP file and copy the Python files into a folder that MicroStation knows about.
Use MicroStation's Python Manager to find and execute the script …
Post questions about MicroStation Python programming to the MicroStation Programming Forum.