pyACAD panel
Console
An interactive Python console inside AutoCAD, with the same pyacad library every tool uses.
On the ribbonButton, pyACAD panel
NeedsNothing (a drawing for anything that reads one)
Command
PYCONSOLEShortcutNone set (bind one in Shortcuts)
What it does#
Opens a window where you type Python and run it inside AutoCAD, in the same process and
with the same pyacad library the ribbon tools use. It is the fastest way to
answer "what does this call return on this drawing" before writing it into a tool, and a
handy calculator for one-off jobs that do not deserve a button. The command is
PYCONSOLE.
How to use it#
- Click Console. The window floats beside AutoCAD and AutoCAD stays usable while it is open.
- Type in the lower pane and press Ctrl+Enter or click
Run. What you ran is echoed with
>>>in the upper pane, then its printed output. When the input ends in an expression its value is shown, as at any Python prompt. An error shows its traceback; nothing else is affected. - Up on the first line and Down on the last line walk through what you have run in this window. In the middle of a multi-line input they move the caret as usual.
- Names you assign survive between runs, so you can build up an answer a line at a time.
from pyacad import blocks, doc, ents, layers, sel
layers.current() # the current layer's name
blocks.counts() # {block name: references}
[ents.type_of(h) for h in sel.current()] # what is selected, by DXF typeGood to know / limits#
- The console runs outside any AutoCAD command, so code that changes the drawing must
lock it first: wrap it in
with doc.lock():. Reading needs no lock. - It is a console, not an editor: there is no file and no save, and history lasts as long as the window. Anything worth keeping belongs in a bundle.
- Code runs on AutoCAD's UI thread, as every tool does. While it runs, Run reads Running and the window shows the wait cursor; a long loop freezes AutoCAD until it finishes.
- This is the IronPython 3.4 engine. A bundle that needs numpy runs on CPython and cannot be tried here.
- Each click opens a new window with its own names and history.
print()output lands in the upper pane. Rich output frompyacad.outputopens in its own window titled pyACAD - Console.PYACADCONSOLEis a different thing: the loader's diagnostic message box (IronPython version, AutoCAD API version, extensions loaded), there for when the ribbon itself failed to load.
Other uses#
- Count something the interface will not: references per block, entities per layer, the layers matching a pattern.
- Make a bulk change that does not deserve a tool:
with doc.lock(): layers.freeze('A-ANNO-*'). - Try a helper from the API reference on the open drawing before you build a button around it. The quickstart turns the lines you keep pasting into a button.
Source#
The tool's own files, word for word as shipped in pyACAD.panel/Console.pushbutton. Every pyACAD tool is an ordinary bundle: copy these into your own extension and change what you like.
script.py Runs on a click. View on GitHub"""Interactive python console running inside AutoCAD with the pyACAD environment.
Built on the DesignSystem v2 toolkit: a solid surface, the real Windows titlebar,
bordered monospace panes and an accent Run button - the same language as every
other pyACAD form.
"""
__title__ = 'Console'
import sys
import clr
from pyacad import _repl, script
from pyacad._history import CommandHistory
clr.AddReference('PresentationFramework')
clr.AddReference('PresentationCore')
clr.AddReference('WindowsBase')
clr.AddReference('WindowsFormsIntegration')
clr.AddReference('PyAcad.Runtime')
from System.Windows import (
CornerRadius, GridLength, GridUnitType, HorizontalAlignment, Thickness,
TextWrapping, VerticalAlignment, Window, WindowStartupLocation,
)
from System.Windows.Controls import (
Border, ColumnDefinition, Grid, RowDefinition, ScrollBarVisibility, TextBox,
)
from System.Windows.Controls.Primitives import ScrollBar
from System.Windows.Input import Cursors, Key, Keyboard, ModifierKeys
from System.Windows.Forms.Integration import ElementHost
from System.Windows.Media import Brushes, FontFamily
from System.Windows.Threading import DispatcherFrame, DispatcherPriority
from System import Action
from System.Windows.Threading import Dispatcher
from PyAcad.Runtime.Forms import DesignSystem, FluentChrome
from PyAcad.Runtime.Output import OutputWindowWriter, PyAcadTheme
# Rich output (pyacad.output) goes to whichever writer the host holds, and that
# is set per RIBBON command - so console output landed in the last button's
# window, under its title. The console owns one writer for its whole life and
# re-asserts it before each run, so output.print_* always lands in a window
# titled for the console.
_output = OutputWindowWriter('pyACAD - Console')
def _pump():
"""Lets WPF finish painting before we block the UI thread on a script."""
frame = DispatcherFrame()
def done():
frame.Continue = False
Dispatcher.CurrentDispatcher.BeginInvoke(
DispatcherPriority.Background, Action(done))
Dispatcher.PushFrame(frame)
_BANNER = (
'pyACAD console - IronPython %s.%s.%s\n'
'Run: Ctrl+Enter . History: Up/Down at the edges. Try: from pyacad import forms, output\n'
% sys.version_info[:3])
# One namespace for the window's lifetime, so assignments survive between runs.
_namespace = {'__pyacad__': __pyacad__}
_history = CommandHistory()
_T = DesignSystem.Tokens.Current
_MONO = FontFamily('Consolas')
def _console_pane(read_only):
"""A bordered v2 frame wrapping a borderless monospace TextBox. Returns (frame, box)."""
box = TextBox()
box.FontFamily = _MONO
box.FontSize = 13
box.AcceptsReturn = True
box.AcceptsTab = True
box.TextWrapping = TextWrapping.NoWrap
box.VerticalScrollBarVisibility = ScrollBarVisibility.Auto
box.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto
box.IsReadOnly = read_only
box.Background = Brushes.Transparent
box.Foreground = DesignSystem.Brush(_T.Ink)
box.BorderThickness = Thickness(0)
box.Padding = Thickness(8)
box.FocusVisualStyle = DesignSystem.FocusRing(DesignSystem.Accent)
box.Resources.Add(clr.GetClrType(ScrollBar), DesignSystem.SlimScrollStyle())
frame = Border()
frame.CornerRadius = CornerRadius(4)
frame.BorderBrush = DesignSystem.Brush(_T.LineStrong)
frame.BorderThickness = Thickness(1)
frame.Background = DesignSystem.Brush(_T.Paper)
frame.Child = box
return frame, box
def _build_window():
window = Window()
window.Title = 'pyACAD Console'
window.Width = 720
window.Height = 540
window.MinWidth = 460
window.MinHeight = 320
window.WindowStartupLocation = WindowStartupLocation.CenterScreen
window.Background = DesignSystem.Brush(_T.Surface)
window.FontFamily = FontFamily('Segoe UI Variable Text, Segoe UI')
FluentChrome.Apply(window) # real Windows titlebar (dark attr in dark theme)
grid = Grid()
grid.Margin = Thickness(16)
for height in (GridLength(1, GridUnitType.Star), GridLength.Auto):
row = RowDefinition()
row.Height = height
grid.RowDefinitions.Add(row)
output_frame, output_box = _console_pane(True)
output_box.Text = _BANNER
Grid.SetRow(output_frame, 0)
grid.Children.Add(output_frame)
# Two columns: the input pane fills the row, Run sits in its own auto column
# so the two never overlap and Run's bottom aligns with the pane's bottom.
input_grid = Grid()
input_grid.Margin = Thickness(0, 12, 0, 0)
Grid.SetRow(input_grid, 1)
fill_col = ColumnDefinition()
fill_col.Width = GridLength(1, GridUnitType.Star)
run_col = ColumnDefinition()
run_col.Width = GridLength.Auto
input_grid.ColumnDefinitions.Add(fill_col)
input_grid.ColumnDefinitions.Add(run_col)
input_frame, input_box = _console_pane(False)
input_frame.MinHeight = 84
input_frame.MaxHeight = 160
Grid.SetColumn(input_frame, 0)
input_grid.Children.Add(input_frame)
run = DesignSystem.Primary(_T, 'Run', None)
run.MinWidth = 0
run.Width = 92
run.Margin = Thickness(12, 0, 0, 0)
run.VerticalAlignment = VerticalAlignment.Bottom
Grid.SetColumn(run, 1)
input_grid.Children.Add(run)
grid.Children.Add(input_grid)
window.Content = grid
def append(text):
output_box.AppendText(text)
output_box.ScrollToEnd()
def run_code(sender, args):
code = input_box.Text.replace('\r\n', '\n').strip()
if not code:
return
_history.add(code)
for line in code.split('\n'):
append('>>> %s\n' % line)
# A script runs on the UI thread (the AutoCAD API demands it), so a
# slow one freezes this window. Say so instead of looking hung: disable
# Run, show the wait cursor, and let WPF paint before we block.
run.IsEnabled = False
run.Content = 'Running'
window.Cursor = Cursors.Wait
try:
_pump()
# Console output belongs in the console's own window, not in whatever
# ribbon button ran last. Guarded: a failure here must never be the
# reason a script does not run.
try:
__pyacad__.SetCurrentOutput(_output)
except Exception:
pass
append(_repl.execute(code, _namespace))
finally:
window.Cursor = None
run.Content = 'Run'
run.IsEnabled = True
input_box.Clear()
input_box.Focus()
def recall(entry):
input_box.Text = entry
input_box.CaretIndex = len(input_box.Text)
def key_down(sender, args):
# PREVIEW, not KeyDown: a TextBox with AcceptsReturn owns Enter (it
# inserts a line break) and the arrow keys (caret movement), and marks
# them handled in class handling, which runs BEFORE instance handlers.
# On the bubbling event this whole function never sees them, so both
# Ctrl+Enter and the history keys are dead. Tunnelling gets there first,
# and Handled=True here also stops the stray newline.
if args.Key == Key.Enter and Keyboard.Modifiers == ModifierKeys.Control:
args.Handled = True
run_code(sender, args)
return
# History: Up on the first line / Down on the last (REPL feel; middle
# lines keep normal caret movement for multiline editing).
if args.Key == Key.Up:
if input_box.GetLineIndexFromCharacterIndex(input_box.CaretIndex) == 0:
args.Handled = True
recall(_history.previous())
elif args.Key == Key.Down:
at_last = (input_box.GetLineIndexFromCharacterIndex(input_box.CaretIndex)
== input_box.LineCount - 1)
if at_last:
args.Handled = True
recall(_history.next())
run.Click += run_code
input_box.PreviewKeyDown += key_down
window.ContentRendered += lambda s, e: input_box.Focus()
return window
# Modeless: AutoCAD stays usable while the console floats. A modeless WPF
# window rides the HOST's message pump, which preprocesses keyboard messages
# itself - most keydowns (digits, Enter) and every WM_CHAR die before the
# window's WndProc, so typing shows nothing and Ctrl+Enter never fires. The
# interop call registers a filter that hands keyboard messages to this window
# first; it must run before Show(). Modal ShowDialog windows pump for
# themselves and do not need it.
_window = _build_window()
ElementHost.EnableModelessKeyboardInterop(_window)
_window.Show()