Following on from this recent post – and inspired by a question we received recently from a developer – I decided to extend the previous code to allow a user to select a portion of a drawing they would like to save to a file or place on the clipboard. This is actually a really useful tool for me when I’m writing this blog, so there was certainly a degree of self-interest involved. :-)
The technique shown in today’s post is actually pretty handy for other situations: it’s quite common to want to transform a point from drawing coordinates (which may well be in a specific User Coordinate System (UCS)) into screen coordinates, especially when wanting to display a window or a custom context menu at the cursor location.
To transform from UCS to screen coordinates we actually need three steps (and thanks to Adam Nagy, from DevTech EMEA, for creating code I borrowed from to get this working):
- Transform the selected point by the current UCS matrix to get WCS
- This can be achieved using the .NET interface
- Transform the WCS point into Windows client coordinates
- For this we need to P/Invoke the ObjectARX function acedCoordFromWorldToPixel()
- Transform the client coordinates into screen coordinates
- For this we need to P/Invoke the Win32 API function ClientToScreen()
We need to do this for both corners of our user-selected window, of course. Once we have the corners in screen coordinates, the code from last time can be used: with a few modifications to support selection of top-right/bottom-left corners, rather than the assumed top-left/bottom-right, as well as adding support for placing the bitmap on the clipboard, rather than saving it to a file.
Here’s the updated C# code with our new WSS command:
using acApp = Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Drawing.Imaging;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Interop;
using System;
namespace ScreenshotTest
{
public class Commands
{
// For the coordinate tranformation we need...
// An ObjectARX function:
[DllImport(
"acad.exe",
EntryPoint="?acedCoordFromWorldToPixel@@YAHHQBNAAVCPoint@@@Z"
)]
static extern int acedCoordFromWorldToPixel(
int windnum,
double[] ptIn,
out Point ptOut
);
// And a Win32 function:
[DllImport("user32.dll")]
static extern bool ClientToScreen(IntPtr hWnd, ref Point pt);
// Command to capture the main and active drawing windows
[CommandMethod("CSS")]
static public void CaptureScreenShot()
{
ScreenShotToFile(
acApp.Application.MainWindow,
0, 0, 0, 0,
"c:\\main-window.png",
false
);
ScreenShotToFile(
acApp.Application.DocumentManager.MdiActiveDocument.Window,
30, 26, 10, 10,
"c:\\doc-window.png",
false
);
}
// Command to capture a user-selected portion of a drawing
[CommandMethod("WSS")]
static public void WindowScreenShot()
{
acApp.Document doc =
acApp.Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Ask the user for the screen window to capture
PromptPointResult ppr =
ed.GetPoint("\nSelect first point of capture window: ");
if (ppr.Status != PromptStatus.OK)
return;
Point3d first = ppr.Value;
ppr =
ed.GetCorner(
"\nSelect second point of capture window: ",
first
);
if (ppr.Status != PromptStatus.OK)
return;
Point3d second = ppr.Value;
// Generate screen coordinate points based on the
// drawing points selected
Point pt1, pt2;
// First we get the viewport number
short vp =
(short)acApp.Application.GetSystemVariable("CVPORT");
// Get the current UCS matrix (would be indentify if in WCS)
Matrix3d ucsMat =
ed.CurrentUserCoordinateSystem;
// And then the handle to the current drawing window
IntPtr hWnd = doc.Window.Handle;
// Now calculate the selected corners in screen coordinates
pt1 = ScreenFromDrawingPoint(ucsMat, hWnd, first, vp);
pt2 = ScreenFromDrawingPoint(ucsMat, hWnd, second, vp);
// Now save this portion of our screen as a raster image
ScreenShotToFile(pt1, pt2, null, true);
}
// Perform our three tranformations to get from UCS (or WCS)
// to screen coordinates
private static Point ScreenFromDrawingPoint(
Matrix3d ucsMat,
IntPtr hWnd,
Point3d ucsPt,
short vpNum
)
{
Point res;
Point3d wcsPt = ucsPt.TransformBy(ucsMat);
acedCoordFromWorldToPixel(vpNum, wcsPt.ToArray(), out res);
ClientToScreen(hWnd, ref res);
return res;
}
// Save the display of an AutoCAD window as a raster file
// and/or an image on the clipboard
private static void ScreenShotToFile(
Autodesk.AutoCAD.Windows.Window wd,
int top, int bottom, int left, int right,
string filename,
bool clipboard
)
{
Point pt = wd.Location;
Size sz = wd.Size;
pt.X += left;
pt.Y += top;
sz.Height -= top + bottom;
sz.Width -= left + right;
SaveScreenPortion(pt, sz, filename, clipboard);
}
// Save a screen window between two corners as a raster file
// and/or an image on the clipboard
private static void ScreenShotToFile(
Point pt1,
Point pt2,
string filename,
bool clipboard
)
{
// Create the top left corner from the two corners
// provided (by taking the min of both X and Y values)
Point pt =
new Point(Math.Min(pt1.X, pt2.X), Math.Min(pt1.Y, pt2.Y));
// Determine the size by subtracting X & Y values and
// taking the absolute value of each
Size sz =
new Size(Math.Abs(pt1.X - pt2.X), Math.Abs(pt1.Y - pt2.Y));
SaveScreenPortion(pt, sz, filename, clipboard);
}
// Save a portion of the screen display as a raster file
// and/or an image on the clipboard
private static void SaveScreenPortion(
Point pt,
Size sz,
string filename,
bool clipboard
)
{
// Set the bitmap object to the size of the window
Bitmap bmp =
new Bitmap(
sz.Width,
sz.Height,
PixelFormat.Format32bppArgb
);
using (bmp)
{
// Create a graphics object from the bitmap
using (Graphics gfx = Graphics.FromImage(bmp))
{
// Take a screenshot of our window
gfx.CopyFromScreen(
pt.X, pt.Y, 0, 0, sz,
CopyPixelOperation.SourceCopy
);
// Save the screenshot to the specified location
if (filename != null && filename != "")
bmp.Save(filename, ImageFormat.Png);
// Copy it to the clipboard
if (clipboard)
System.Windows.Forms.Clipboard.SetImage(bmp);
}
}
}
}
}
To get the code working, you may need to add an additional assembly reference to System.Windows.Forms.dll, which gives us access to the Clipboard. I should also mention that I’m running this code on AutoCAD 2010 on a 32-bit OS: if the DllImport statement to P/Invoke our ObjectARX function, you may need to find and declare the appropriate function signature.
To put our WSS command through its paces, let’s load a drawing and set a random UCS (I do this by changing the view using 3DORBIT and using the UCS command, setting it to the current View) and on top of that changing the view to be from a another random angle not aligned with this new UCS:
Now we NETLOAD our assembly and run the WSS command, selecting the two corners of our window:
And we can see that the resultant image is on the clipboard by pasting it somewhere (we could very easily adjust the code to save to a file, if we so chose) :
Update:
Paavo pointed out in a comment that I had missed a managed API method to replace the P/Invoke of acedCoordFromWorldToPixel(), which makes the code much cleaner and more portable across versions: Editor.PointToScreen(). We still have to P/Invoke ClientToScreen(), but at least that’s undecorated/unmangled.
Here’s the revised C# code, which I’ve included in its entirety because it has ended up meaning a change both to the ScreenFromDrawingPoint() function and the code that calls it: we need the editor to perform our transformation so we might as well use it within the function to retrieve the matrix representing the current UCS. Which simplifies things somewhat.
using acApp = Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Drawing.Imaging;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Interop;
using System;
namespace ScreenshotTest
{
public class Commands
{
// For the coordinate tranformation we need...
// A Win32 function:
[DllImport("user32.dll")]
static extern bool ClientToScreen(IntPtr hWnd, ref Point pt);
// Command to capture the main and active drawing windows
[CommandMethod("CSS")]
static public void CaptureScreenShot()
{
ScreenShotToFile(
acApp.Application.MainWindow,
0, 0, 0, 0,
"c:\\main-window.png",
false
);
ScreenShotToFile(
acApp.Application.DocumentManager.MdiActiveDocument.Window,
30, 26, 10, 10,
"c:\\doc-window.png",
false
);
}
// Command to capture a user-selected portion of a drawing
[CommandMethod("WSS")]
static public void WindowScreenShot()
{
acApp.Document doc =
acApp.Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Ask the user for the screen window to capture
PromptPointResult ppr =
ed.GetPoint("\nSelect first point of capture window: ");
if (ppr.Status != PromptStatus.OK)
return;
Point3d first = ppr.Value;
ppr =
ed.GetCorner(
"\nSelect second point of capture window: ",
first
);
if (ppr.Status != PromptStatus.OK)
return;
Point3d second = ppr.Value;
// Generate screen coordinate points based on the
// drawing points selected
Point pt1, pt2;
// First we get the viewport number
short vp =
(short)acApp.Application.GetSystemVariable("CVPORT");
// Then the handle to the current drawing window
IntPtr hWnd = doc.Window.Handle;
// Now calculate the selected corners in screen coordinates
pt1 = ScreenFromDrawingPoint(ed, hWnd, first, vp);
pt2 = ScreenFromDrawingPoint(ed, hWnd, second, vp);
// Now save this portion of our screen as a raster image
ScreenShotToFile(pt1, pt2, null, true);
}
// Perform our three tranformations to get from UCS (or WCS)
// to screen coordinates
private static Point ScreenFromDrawingPoint(
Editor ed,
IntPtr hWnd,
Point3d ucsPt,
short vpNum
)
{
Point3d wcsPt =
ucsPt.TransformBy(
ed.CurrentUserCoordinateSystem
);
Point res = ed.PointToScreen(wcsPt, vpNum);
ClientToScreen(hWnd, ref res);
return res;
}
// Save the display of an AutoCAD window as a raster file
// and/or an image on the clipboard
private static void ScreenShotToFile(
Autodesk.AutoCAD.Windows.Window wd,
int top, int bottom, int left, int right,
string filename,
bool clipboard
)
{
Point pt = wd.Location;
Size sz = wd.Size;
pt.X += left;
pt.Y += top;
sz.Height -= top + bottom;
sz.Width -= left + right;
SaveScreenPortion(pt, sz, filename, clipboard);
}
// Save a screen window between two corners as a raster file
// and/or an image on the clipboard
private static void ScreenShotToFile(
Point pt1,
Point pt2,
string filename,
bool clipboard
)
{
// Create the top left corner from the two corners
// provided (by taking the min of both X and Y values)
Point pt =
new Point(Math.Min(pt1.X, pt2.X), Math.Min(pt1.Y, pt2.Y));
// Determine the size by subtracting X & Y values and
// taking the absolute value of each
Size sz =
new Size(Math.Abs(pt1.X - pt2.X), Math.Abs(pt1.Y - pt2.Y));
SaveScreenPortion(pt, sz, filename, clipboard);
}
// Save a portion of the screen display as a raster file
// and/or an image on the clipboard
private static void SaveScreenPortion(
Point pt,
Size sz,
string filename,
bool clipboard
)
{
// Set the bitmap object to the size of the window
Bitmap bmp =
new Bitmap(
sz.Width,
sz.Height,
PixelFormat.Format32bppArgb
);
using (bmp)
{
// Create a graphics object from the bitmap
using (Graphics gfx = Graphics.FromImage(bmp))
{
// Take a screenshot of our window
gfx.CopyFromScreen(
pt.X, pt.Y, 0, 0, sz,
CopyPixelOperation.SourceCopy
);
// Save the screenshot to the specified location
if (filename != null && filename != "")
bmp.Save(filename, ImageFormat.Png);
// Copy it to the clipboard
if (clipboard)
System.Windows.Forms.Clipboard.SetImage(bmp);
}
}
}
}
}