I am back from a nice break in Italy, including a visit to the Giardino dei Tarocchi, the Tarot Garden sculpture garden based on the tarot cards, created by Niki de Saint Phalle (photos):
Before I get back to work properly attending the European Autodesk University in Darmstadt, Germany, here is a note from Hank DiVincenzo, Sr. Software Engineer at Ideate, Inc, on keeping Revit focused and on top when working with a modeless form, and an important heads-up warning from the Revit development team on a future change coming:
We here at Ideate Software are seeing what appears to be Revit add-in ownership issues with Revit's main window.
The behavior has changed between Revit 2017 and Revit 2018 for modeless add-ins.
For Revit 2018, when a modeless add-in is closed, Revit does not retain is focus; it is pushed behind another application.
With some experimentation, it was found the application that had focus before Revit is the one that gains focus after the modeless add-in is closed.
Revit 2016 and 2017 do not show this behavior, only Revit 2018 does.
Also Note:
What we understand as the preferred method of getting Revit's MainWindowHandle has issues when there are multiple modeless add-ins opened.
When the MainWindowHandle is gotten from the Revit process (i.e. using the Process.GetCurrentProcess()
MainWindowHandle
property), modeless add-in ownership is incorrect.
Any modeless add-in that is started after the first one has its ownership set to that previous modeless add-in. This can be seen by closing any of the interim modeless add-ins and any modeless add-ins started after the one being closed, also closes.
What I have called 'owner' is also known as the parent-child window relationship.
Here is the write-up of the two window issues we ran into and have solved.
To demonstrate, I am also sharing the Visual Studio 2013 project ModelessAddinSolution.zip.
It implements an add-in that can be used to demonstrate the focus issue and its solution following the instructions provided below. It also includes the add-in manifest .addin
file in the External Application folder.
This describes my journey to solve two issues with Revit add-in windows.
The first issue was a general problem seen in all version of Revit, the second issue was specific to Revit 2018.
While developing a second modeless add-in, we ran into an issue with add-in window parenting/ownership. The problem manifested itself when having two modeless add-ins open: when the first add-in opened was closed, the second add-in opened also closed. With this behavior, it was apparent the second add-in was somehow being associated with the first add-in. We were using Process.GetCurrentProcess().MainWindow to get main Revit window. Somehow, once the first modeless add-in was opened, the MainWindow
property for the Revit process changed. A new method for getting Revit's main window was needed.
An alternate solution was needed to get the window handle for Revit's main window, the window that holds the ribbon and is the first window opened.
The following solution was implemented:
The thought here is windows that have no parent are those opened by the OS and not another part of Revit.
This method seems fragile because it depends on text from Revit's main window title, but it consistently gets the Revit main window no matter what add-ins are open. And localization is not an issue, the main window title text “Autodesk Revit” does not change for other languages, cf. the following two language examples.
French:
German:
Class usage within the add-in's main window; the add-in is WPF a app:
// Set Revit as owner of given child window WindowHandleSearch handle = WindowHandleSearch.MainWindowHandle; handle.SetAsOwner(this);
Main Window Handle Class:
/// <summary> /// Wrapper class for window handles /// </summary> public class WindowHandleSearch : IWin32Window, System.Windows.Forms.IWin32Window { #region Static methods /// <summary> /// Revit main window handle /// </summary> static public WindowHandleSearch MainWindowHandle { get { // Get handle of main window var revitProcess = Process.GetCurrentProcess(); return new WindowHandleSearch( GetMainWindow( revitProcess.Id ) ); } } #endregion #region Constructor /// <summary> /// Constructor - From WinForms window handle /// </summary> /// <param name="hwnd">Window handle</param> public WindowHandleSearch( IntPtr hwnd ) { // Assert valid window handle Debug.Assert( IntPtr.Zero != hwnd, "Null window handle" ); Handle = hwnd; } #endregion #region Methods /// <summary> /// Window handle /// </summary> public IntPtr Handle { get; private set; } /// <summary> /// Set this window handle as the owner /// of the given window /// </summary> /// <param name="childWindow">Child window /// whose parent will be set to be this window /// handle</param> public void SetAsOwner( Window childWindow ) { var helper = new WindowInteropHelper( childWindow ) { Owner = Handle }; } // User32.dll calls used to get the Main Window for a Process Id (PID) private delegate bool EnumWindowsProc( HWND hWnd, int lParam ); [DllImport( "user32.DLL" )] private static extern bool EnumWindows( EnumWindowsProc enumFunc, int lParam ); [DllImport( "user32.dll", ExactSpelling = true, CharSet = CharSet.Auto )] private static extern IntPtr GetParent( IntPtr hWnd ); [DllImport( "user32.dll", SetLastError = true )] private static extern uint GetWindowThreadProcessId( IntPtr hWnd, out uint processId ); [DllImport( "user32.DLL" )] private static extern IntPtr GetShellWindow(); [DllImport( "user32.DLL" )] private static extern bool IsWindowVisible( HWND hWnd ); [DllImport( "user32.DLL" )] private static extern int GetWindowTextLength( HWND hWnd ); [DllImport( "user32.DLL" )] private static extern int GetWindowText( HWND hWnd, StringBuilder lpString, int nMaxCount ); /// <summary> /// Returns the main Window Handle for the /// Process Id (pid) passed in. /// IF the Main Window is not found then a /// handle value of Zreo is returned, no handle. /// </summary> /// <param name="pid"></param> /// <returns></returns> public static IntPtr GetMainWindow( int pid ) { HWND shellWindow = GetShellWindow(); List<HWND> windowsForPid = new List<IntPtr>(); try { EnumWindows( // EnumWindowsProc Function, does the work // on each window. delegate ( HWND hWnd, int lParam ) { if( hWnd == shellWindow ) return true; if( !IsWindowVisible( hWnd ) ) return true; uint windowPid = 0; GetWindowThreadProcessId( hWnd, out windowPid ); // if window is from Pid of interest, // see if it's the Main Window if( windowPid == pid ) { // By default Main Window has a // Parent Window of Zero, no parent. HWND parentHwnd = GetParent( hWnd ); if( parentHwnd == IntPtr.Zero ) windowsForPid.Add( hWnd ); } return true; } // lParam, nothing, null... , 0 ); } catch( Exception ) { } return DetermineMainWindow( windowsForPid ); } /// <summary> /// Finds Revit's Main Window from the list of /// window handles passed in. /// If the Main Window for Revit is not found /// then a Null (IntPtr.Zero) handle is returnd. /// </summary> /// <param name="handles"></param> /// <returns></returns> private static IntPtr DetermineMainWindow( List<HWND> handles ) { // Safty conditions, bail if not met. if( handles == null || handles.Count <= 0 ) return IntPtr.Zero; // default Null handel HWND mainWindow = IntPtr.Zero; // only one window so return it, // must be the Main Window?? if( handles.Count == 1 ) { mainWindow = handles[0]; } // more than one window else { // more than one candidate for Main Window // so find the Main Window by its Title, it // will contain "Autodesk Revit" foreach( var hWnd in handles ) { int length = GetWindowTextLength( hWnd ); if( length == 0 ) continue; StringBuilder builder = new StringBuilder( length ); GetWindowText( hWnd, builder, length + 1 ); // Depending on the Title of the Main Window // to have "Autodesk Revit" in it. if( builder.ToString().ToLower().Contains( "autodesk revit" ) ) { mainWindow = hWnd; break; // found Main Window stop and return it. } } } return mainWindow; } #endregion }
With the main window found and working for Revit 2016 and 2017, all was good and a couple more WPF modeless applications were developed. Then came Revit 2018...
While porting our add-in(s) to Revit 2018, we noticed Revit lose focus from time to time. When our modeless (WinForms based) add-in window was closed, Revit was not left on top with focus; instead, the application that had focus before Revit was placed on top. This was a surprise, being something that Windows should handle. Notable was this new losing focus behavior was only seen in Revit 2018, and not in 2017 or 2016.
An example of the sequence would be:
File Manager would now be placed on top, i.e., Revit lost its position in the application stack/show, and was replaced by File Manager.
The above was the case for the WinForms based add-ins, but WPF based add-ins needed additional steps:
File Manager would then be placed on top, replacing Revit as the top application.
The resolution for the loss of focus ended up being somewhat simple.
I registered for the OnClosing
event in the add-in's main window. Within the OnClosing
event handler, I set Revit to be on top with the User32.dll call SetForegroundWindow
.
Because the owner/parent of the add-in window was set correctly for Revit, setting the add-in's owner (Revit) on closing to be in the foreground solved the focus problem.
NOTE: See the section “Code for Second Resolution” below for the solution to this issue.
To Run the Sample Add-In:
/// <summary> /// User32 calls used to set Revit focus /// </summary> [DllImport( "USER32.DLL" )] internal static extern bool SetForegroundWindow( HWND hWnd ); /// <summary> /// Use the OnClose event to ensure Revit is brought back into focus. /// </summary> /// <param name="e"></param> protected override void OnClosing( CancelEventArgs e ) { // do the base work first. base.OnClosing( e ); // Set Revit to the foreground try { IntPtr ownerIntPtr = new WindowInteropHelper( this ).Owner; if( ownerIntPtr != IntPtr.Zero ) User32.SetForegroundWindow( ownerIntPtr ); } catch( Exception ) { } }
Very many thanks to Hank for his persistent research and sharing this valuable solution.
I myself always used the JtWindowHandle
class to retrieve the main Revit window handle in the past, and use that
to set up the parent window relationship for my modeless forms for
simple single modeless forms.
I have been warned by the development team that this method will cease to work in the next major release of Revit, after Revit 2018:
A developer followed the advice to ensure a WPF add-in remains in foreground in building a new add-in.
This was written about 5 years ago.
As of the next major release of Revit, the method described there will no longer work.
An easy way to fix this will be provided. The Revit API will include new API calls providing an official way to get the application window handle:
MainWindowHandle
property.