Today is the last day of the Munich AEC DevLab. It has been a great week here, both meeting with developers to discuss their issues, find quite a few solutions, and connect again with many old friends here in the Munich Autodesk office. One issue that came up repeatedly was also a topic in the Waltham DevLab, so it is well worth exploring here in more detail:
People have repeatedly asked how to properly hook up their .NET forms to the Revit main application window so that it is always displayed on top of Revit and reacts properly when Revit is minimised and restored. The technique presented below works equally well for modal and modeless forms. Here is an example of such a query:
Question: I am displaying a dialogue that I want to associate with Revit. Is there a way to explicitly specify the Revit window with the call to the ShowDialog method so that my form is associated with the Revit window instance and session?
Answer: I believe that the simplest way to achieve what you need is the following:
- Determine the Revit main application window handle.
- Implement an IWin32Window wrapper class returning the window handle.
- Provide the IWin32Window instance as an argument to the ShowDialog method.
Let us look at these steps one by one:
1. If you are inside a Revit add-in, e.g. an external command, so that your code is part of Revit, so to speak, you can access the HWND window handle of the main Revit application using the following code:
Process process = Process.GetCurrentProcess(); IntPtr h = process.MainWindowHandle;
2. The Windows.Forms Form.Show and ShowDialog methods are overloaded. You can call them without an argument, or with an instance of an IWin32Window interface implementation. That interface requires you to implement the Handle method, which can return the window handle determined in step 1. Here is an example of such a wrapper class implementation, with its constructor taking an IntPtr argument representing a HWND window handle:
public class JtWindowHandle : IWin32Window { IntPtr _hwnd; public JtWindowHandle( IntPtr h ) { Debug.Assert( IntPtr.Zero != h, "expected non-null window handle" ); _hwnd = h; } public IntPtr Handle { get { return _hwnd; } } }
I presented an example of implementing and using such a wrapper class in the discussion on driving Revit from outside. It demonstrates an alternative but less reliable method based on FindWindow to determine the main Revit window handle.
3. Provide an instance of your IWin32Window class to your form's Show or ShowDialog method.
form.ShowDialog( new JtWindowHandle( h ) );
Now your dialogue will be correctly recognised as a child window of the main Revit application window:
- The dialogue remains on top of the Revit window when it is not minimised.
- The dialogue is automatically minimised when Revit is minimised, and restored again when Revit is restored.
I am attaching my LooseConnectors sample application that I recently created for the AEC DevCamp Revit MEP API presentation. It makes use of the MEP element collector and connector retrieval methods that I presented a few days ago and then displays a list of all unconnected connectors in a modeless dialogue box. It ensures that it stays on top of the Revit window by passing in the Revit window handle to the Form.Show method via an IWin32Window wrapper class, thus making Revit its parent window. This requires converting the Windows API HWND which is represented as an IntPtr in .NET to an IWin32Window instance which is the incarnation preferred in the .NET System.Windows.Forms environment. I will return to a more in depth discussion of this sample and various other of its interesting features and implementation aspects soon.
Here is some response to suggestions similar to those above:
Response: Thank you for the tip on making my form a child of the Revit window. I did not realize that the Show method could simply take an argument. Your wrapper for the IWin32Window was very helpful.
I discovered one remaining problem with handling a double click in a tree view, and implemented a workaround for that.
I was invoking a routine to place a family instance from a double-click event on a tree view in my form. It turns out that almost anything else works perfectly but not double-click. I call it from the single-click of a button, and smoothly drag in my instance. Same thing for a single-click in the list, or a right-click. It seems that something about the double-click is pulling the focus back to my form, probably some kind of timing issue. Clearly a windows forms issue, and nothing to do with Revit.
For now, I have changed my interface so the user selects the appropriate item in the list and right-clicks, and everything runs smoothly. I thought you might want to know about this in case someone else runs into a similar glitch and can save some aggravation. In the long run, I may want to figure out how to make it work with a double-click, but for the moment, I will move on to the many other challenges.
After much searching I found the WPF window.owner property. I was unable to set the Revit window as the owner property because it requires a Window and not a IntPtr. Then I recently found the WindowInteropHelper class and was able to set the Owner property of the WPF window with The IntPtr similar to the above post. Two lines of code to implement prior to showing the window.
public void Run()
{
MyModelessWindow = new MyModelessWindow();
System.Windows.Interop.WindowInteropHelper x = new System.Windows.Interop.WindowInteropHelper(MyModelessWindow);
x.Owner = Process.GetCurrentProcess().MainWindowHandle;
MyModelessWindow.Show();
}
I'm quite happy about this discovery so I thought I'd share it.
Posted by: Mr West | January 18, 2012 at 17:19
Dear Mr West,
I'm quite happy for you too, and more than happy that you decided to share it :-)
Thank you!
Cheers, Jeremy.
Posted by: Jeremy Tammik | January 19, 2012 at 01:35
Other way to get Revit main window handle (I think its easier):
1) Add reference to the AdWindows assembly (located at the same place where RevitAPI assembly) and set Copy Local property to false
2) Get Revit main window handle using static property ApplicationWindow of ComponentManager class in Autodesk.Windows namespace.
Now, you can use only one Property (ComponentManager.ApplicationWindow) instead two methods (get current process and main window handle)
Best regards, Victor.
Posted by: Account Deleted | March 20, 2012 at 04:35
Dear Victor,
This is absolutely beautiful, much nicer indeed!
I think we should promote this information to a main blog post right away.
Thank you!
Cheers, Jeremy.
Posted by: Jeremy Tammik | March 20, 2012 at 08:51
Hello, Jeremy.
I've found many interesting classes and methods in AdWindows and UIFramework assemblies. Unfortunately it doesn't documented.
Now I use in me projects next:
1) ComponentManager.ApplicationWindow
2) UIFramework.RevitRibbonControl.RibonControl to determine if RebbonTab exists
3) Autodesk.Windows.TaskDialog - more flexible TaskDialog than Autodesk.Revit.UI.TaskDialog.
Best regards, Victor
Posted by: Account Deleted | March 20, 2012 at 23:13
Dear Victor,
Thank you, that sounds interesting indeed.
I did publish your previous hint about the parent window:
http://thebuildingcoder.typepad.com/blog/2012/03/melbourne-day-two.html#5
I think the other possibilities you mention sound exciting as well.
Please let us know if you implement anything that you think might be useful and worthwhile sharing, I'll be more than glad to publish them.
Thank you!
Cheers, Jeremy.
Posted by: Jeremy Tammik | April 05, 2012 at 12:09
Hello Jeremy,
In Revit 2014 I'm having trouble showing a modeless form and calling a transaction whilst inside the form. It is giving me the error.
"starting a transation from an external application running outside of api context is not allowed"
I am using your method to send IWin32Window to show the form.
Any Ideas?
Kind Regards
David
Posted by: David Rock | May 08, 2013 at 00:05
Dear David,
Thank you for your query, both via ADN and this comment.
It looks as if you are calling the Revit API directly from your modeless dialog that is running in another thread.
As you should know, actually, that was never supported.
Now, in Revit 2014, an exception is raised when you make such an attempt.
You are lucky that it worked so far without corrupting anything.
The solution is described in depth on The Building Coder: simply google for "modeless site:thebuildingcoder.typepad.com".
In short, make use of the Idling event, or implement an external event, which is a simplified wrapper around that, and base you application on the ModelessForm_ExternalEvent and ModelessForm_IdlingEvent SDK samples.
I hope this helps.
Cheers, Jeremy.
Posted by: Jeremy Tammik | May 08, 2013 at 02:48