Saturday, February 25, 2017

Free Source Code to Export Project Warnings with the API

From Matteo Cominetti's website:

The warnings list in Revit is not accessible via the API, at least for versions earlier than Revit 2018. Using the Win32 API, I’ve managed to circumvent this limitation by simulating user clicks on the interface to trigger the export of the html warnings list to a custom location. You can then use Html Agility Pack or other libraries to parse the html table to get the information you need.

The method ExportWarinings in the class below stores the exported html files to the user’s temp folder and returns its location , you can invoke it with:

var htmlPath = await Win32Api.ExportWarinings(uiapp);

And here the full code I have used:

  /// <summary>
  /// Run Revit commands using Win32 API
  /// </summary>
  public class Win32Api
    private static extern IntPtr GetForegroundWindow();

    static extern bool SetFocus(IntPtr hWnd);

    [DllImport("user32.dll", SetLastError = false)]
    public static extern IntPtr GetDlgItem(IntPtr hDlg, int nIDDlgItem);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SendMessage(IntPtr hwnd, uint Msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
    public static extern IntPtr SendMessage(HandleRef hWnd, uint Msg, IntPtr wParam, string lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

    public static extern bool PostMessage(IntPtr hWnd, uint msg, uint wParam, uint lParam);

    public const Int32 BM_CLICK = 0x00F5;
    public const Int32 WM_SETTEXT = 0x000C;
    public const Int32 VK_SPACE = 0x20;
    public const Int32 WM_KEYDOWN = 0x100;

    internal static async Task<string> ExportWarinings(UIApplication uiapp)
      //paths stuff
      var tempDir = Path.Combine(Path.GetTempPath(), "RevitWarnings");
      var path = Path.Combine(tempDir, "warning_" + Guid.NewGuid()+".html");
      if (!Directory.Exists(tempDir))
      //get revit command to open the warnings window
      var id = RevitCommandId.LookupPostableCommandId(PostableCommand.ReviewWarnings);
      IntPtr mainWin = GetForegroundWindow();

      //wait until the new window gets in the foreground
      while (GetForegroundWindow()== mainWin)
        await DelayWork(100);
      IntPtr warningsWindow = GetForegroundWindow();

      //must be before the thread-blocking export dialog, and must be without 'await'
      Task.Run(() => TypeSaveFileName(path));
      //click the export button, will show a modal window to Save As
      ClickControl(warningsWindow, "&Export...");
      //after the save dialog is closed, close the warnings window
      ClickControl(warningsWindow, "Close");

      return path;

    /// <summary>
    /// The Save As is shown as modal dialog and it blocks the normal execution of the code
    /// This function is executed on a new thread with a small delay so that it is not blocked and it can
    /// set the file path an then click Save.
    /// </summary>
    /// <param name="text"></param>
   private static async void TypeSaveFileName(string text)
      //wait until the new window gets in the foreground
      IntPtr mainWin = GetForegroundWindow();
      while (GetForegroundWindow() == mainWin)
        await DelayWork(100);
      IntPtr exportWindow = GetForegroundWindow();
      IntPtr comboHandle = GetDlgItem(exportWindow, 13006);
      IntPtr editHandle = GetDlgItem(comboHandle, 1001);
      SetControlText(comboHandle, 1001, text);
      //need to simulate a keystroke or the path won't come throught
      PostMessage(editHandle, WM_KEYDOWN, VK_SPACE, 0);
      await DelayWork(100);
      ClickControl(exportWindow, "&Save");

    /// <summary>
    /// Clicks a button using Win32API
    /// </summary>
    /// <param name="windowHandle">Handle of the windows the control belongs to</param>
    /// <param name="buttonText">Label of the button, to get it correctly use WinSpy</param>
    private static void ClickControl(IntPtr windowHandle, string buttonText)
      IntPtr export = FindWindowEx(windowHandle, IntPtr.Zero, null, buttonText);
      SendMessage(export, BM_CLICK, IntPtr.Zero, IntPtr.Zero);

    /// <summary>
    /// Sets the text value of a control using Win32API
    /// </summary>
    /// <param name="windowHandle">Handle of the windows the control belongs to</param>
    /// <param name="controlId">ID of the control, to get it use WinSpy and convert the value fron Hex to  Decimal</param>
    /// <param name="text">String to be set</param>
    private static void SetControlText(IntPtr windowHandle, int controlId, string text)
      IntPtr iptrHWndControl = GetDlgItem(windowHandle, controlId);
      HandleRef hrefHWndTarget = new HandleRef(null, iptrHWndControl);
      SendMessage(hrefHWndTarget, WM_SETTEXT, IntPtr.Zero, text);

    /// <summary>
    /// Simple function to delay the code async, so that the windows can be loaded
    /// </summary>
    /// <param name="i"></param>
    /// <returns></returns>
    private static async Task DelayWork(int i)
      await Task.Delay(i);

Running the method does the following actions:
  • generate a unique temp path
  • run PostCommand to open the warnings windows
  • clicks the “Export” button
  • on a separate thread, sets the temp path as file name and clicks save
  • closes the warnings dialog and returns the path

Delays of 500ms have been set in between some actions to allow windows to be displayed correctly before triggering the mouse clicks, but these values might need to be increased on slower machines.

Have fun with your warnings!

There's more information available on Matteo Cominetti's website.

No comments: