Inspired by the Windows 8 conference I attended on Monday, I’ve decided to build my first Metro-style application which scrolls through AutoCAD’s Most-Recently-Used (MRU) drawing list.
I now have a barebones installation of the Windows 8 Consumer Preview + VS11 Beta inside a Parallels VM on my Mac, but rather than installing AutoCAD into that, I decided to write a simple exporter on my existing Windows 7 machine (which now has AutoCAD 2013 installed) to generate the data for my Metro-style application.
Let’s start by looking at what file-oriented MRU data AutoCAD stores, and where.
At the end of each session, AutoCAD stores MRU information about file accesses in the Registry:
For the version of AutoCAD I now have installed it’s stored under this key:
HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R19.0\ACAD-B001:409\Recent File List
In there you’ll find various information:
- File1 – File35: the paths to the 35 most recently accessed files (which includes DWGs and sheet sets)
- Class1 – Class35: the classification of these files (which so far always seems to be AcApImpRecentDocument)
- FilePinned1 – FilePinned35: whether these files have been pinned by the user
- FileTime1 – FileTime35: the UNIX time stamp indicating when these files were last accessed
(This information is based on my observations, and – as is always the case when dealing with the Registry directly rather than a public API – is subject to change.)
Getting the information from this Registry location is pretty straightforward, especially if working from within AutoCAD, as we can use HostApplicationServices.Current.RegistryProductRootKey to provide access to the base Registry location (this has changed to HostApplicationServices.Current.UserRegistryProductRootKey in AutoCAD 2013, where we’ve also exposed a comparable MachineRegistryProductRootKey property from HostApplicationServices).
We need some code that will go through this list and store some information to the file system:
- A single XML file containing the name of each drawing and information about when it was last accessed
- A thumbnail PNG of each drawing (which also needs to be referred to from the XML)
A couple of approaches stood out when considering options for doing this “export”:
- Aside from collecting the file metadata and storing it to XML, generate a script to OPEN, PNGOUT and CLOSE each drawing and then fire up the Core Console to execute it in the background
- Create a CRX application to be NETLOADed directly into the Core Console, which can side-load each drawing into a Database to access its properties (including the thumbnail image)
I ended up opting for the second approach: while the first is valid for many types of operation, it seemed overkill to manually generate a PNG for each drawing when I could access it from the DWG directly. And I also wasn’t sure whether the act of opening the drawing in the Core Console would modify the MRU or not (I suspect not, as that’s really a UI-related function, but it didn’t seem to be worth finding out).
And besides, I really wanted to see what it was like to implement a CRX and debug using the Core Console rather than full AutoCAD.
A quick refresher… a CRX is more than a DBX but less than an ARX, in that it can be loaded by applications that host AcCore.dll (which includes AutoCAD and the Core Console but not applications using RealDWG for file access). The .crx extension is used for native files – just as .arx and .dbx is – so our .NET class library will simply be a .dll that gets NETLOADed into AutoCAD 2013 or its Core Console.
Creating a Core-capable DLL was easy: I just removed my project reference to AcMgd.dll and added one to AcCoreMgd.dll (making sure the Project’s Reference Paths pointed to AutoCAD 2013 or its ObjectARX SDK, rather than AutoCAD 2012).
Here’s the C# code for the application:
using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Microsoft.Win32;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Drawing;
using System.IO;
using System;
namespace MruExtraction
{
public class Commands
{
const string basePath = "c:\\temp\\";
const char stringSep = '|';
private string[] GetMruFiles()
{
List<string> files = new List<string>();
RegistryKey ack =
Registry.CurrentUser.OpenSubKey(
HostApplicationServices.Current.UserRegistryProductRootKey,
true
);
using (ack)
{
RegistryKey mruk = ack.CreateSubKey("Recent File List");
using (mruk)
{
for (int i = 1; i <= 35; i++)
{
string file =
(string)mruk.GetValue("File" + i.ToString());
if (string.IsNullOrEmpty(file))
break;
else
{
if (Path.GetExtension(file) == ".dwg")
{
string fileTime =
(string)mruk.GetValue("FileTime" + i.ToString());
files.Add(file + stringSep + fileTime);
}
}
}
}
}
return files.ToArray();
}
public static DateTime UnixTimeStampToDateTime(long timeStamp)
{
// Unix timestamp is seconds past epoch
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0);
return epoch.AddSeconds(timeStamp).ToLocalTime();
}
[CommandMethod("EMI")]
public void ExtractMruInformation()
{
string[] files = GetMruFiles();
int created = 0;
StreamWriter sw = File.CreateText(basePath + "mru.xml");
sw.WriteLine("<MruFiles>");
using (sw)
{
for (int i = 0; i < files.Length; i++)
{
int sepLoc = files[i].IndexOf(stringSep);
string file = files[i].Substring(0, sepLoc);
string fileTime = files[i].Substring(sepLoc + 1);
DateTime lastRead =
UnixTimeStampToDateTime(long.Parse(fileTime));
using (Database db = new Database(false, true))
{
db.ReadDwgFile(
file, FileOpenMode.OpenForReadAndReadShare, false, ""
);
Bitmap thumb = db.ThumbnailBitmap;
if (thumb != null)
{
// Generate a filename and save the image to it
string imageFile = "MruImage" + i.ToString() + ".png";
thumb.Save(basePath + imageFile, ImageFormat.Png);
// Get the time the drawing was last modified
DateTime lastMod = File.GetLastAccessTime(file);
// Get the base file name
string baseName =
Path.GetFileNameWithoutExtension(file);
// Write a line of XML for each file
sw.WriteLine(
"<MruFile Name=\"{0}\" " +
"LastAccess=\"{1}\" " +
"DayOfWeek=\"{2}\" " +
"LastEdit=\"{3}\" " +
"File=\"{4}\" />",
baseName,
lastRead.ToShortDateString(),
lastRead.DayOfWeek,
lastMod.ToShortDateString(),
imageFile
);
// Increment our count to report at the end
created++;
}
}
}
sw.WriteLine("</MruFiles>");
sw.Close();
}
Application.DocumentManager.MdiActiveDocument.Editor.
WriteMessage(
"\nExtracted thumbnails for {0} drawing{1}" +
" in the MRU list.",
created, created == 1 ? "" : "s"
);
}
}
}
You’ll notice the code is very similar to what would have worked in a full AutoCAD application – the main difference was the adjusted namespace to make use of the Application object (Autodesk.AutoCAD.ApplicationServices.Core).
To debug the application, I pointed my project settings to the accoreconsole.exe executable (in the same location as acad.exe) and that was it. I could have used the Autoloader or demand-loading to automatically load the DLL, but chose simply to use the NETLOAD command, instead (to which I have an alias defined in my acad.pgp file, so I can use NL rather than typing NETLOAD each time – I was very happy to see the Core Console respect these aliases :-).
Debugging itself was great: it was very quick to launch the app and debug into code (it would have been even nicer if I’d taken the time to set up automatic loading).
Here’s our EMI command executing inside the Core Console – you’ll notice a bit of quirkiness when typing (or, in my case, pasting) the path to the DLL into the console when it’s requested by NETLOAD, but it’s purely cosmetic:
When we check the output folder, we’ll see our files:
Including the XML with our metadata:
Now we have some usable files, I’ll start working on a simple grid-view WinRT app to see what that’s like.