📜 ⬆️ ⬇️

Non-standard approach to standard add-on development (Add-In) in C #

Using Add-In'a you can implement additional functionality, which greatly facilitates the daily use of the application. The standard approach is simple - we study the API of the application for developing an add-on and implement it as a separate library. The choice is usually offered in several languages, C # is likely to be in this list because of its prevalence and popularity for developing applications for Windows on the .Net platform.

However, it is often necessary that the add-on constantly display some kind of visual information needed when using the application. With a simple solution to this problem — creating a custom window and displaying it either on top of all windows, or moving it to the foreground — we get some inconvenience with the organization of user space. They can somehow be tolerated, but what if there should be several such windows?

As a rule, in the application environment itself everything is organized as it should - the necessary actions are implemented in the form of additional windows, the location and size of which the user wants and adjusts for himself. It can be assumed that the API should contain this functionality of integrating custom windows into the environment, but oddly enough, most likely this functionality will be absent. But, if very necessary, it can be implemented.

At the heart of any Windows application is an event model - the application receives messages (both from the user and the operating system) and responds to them, for example, draws its content. All this functionality is implemented in the window procedure, thus, by replacing this procedure with our user procedure, we can completely change the window operation algorithm. To perform such a low-level task, it is necessary to refer to the functions of the operating system - WinAPI. Communication C # (. NET Framework) and WinAPI is implemented using Platform Invoke services - the ability to call from the managed code (C #) the unmanaged code of WinAPI procedures (C).
')
Consider for example the development of add-ons for professional CAD Eplan. It is assumed that there is already experience in developing a standard add-on (if not, you can familiarize yourself with the basics here ), therefore, only actions related to the implementation of the add-on visual window will be commented. Consider, for a start, an add-on option that displays a list of currently open pages in a separate window (in the foreground).

Main add-on program file, main.cs.

//@file main.cs //@brief ,  . // @par  : // @$Rev: 1 $.\n // @$Author: idimm $.\n // @$Date:: 2015-12-03 12:05:13#$. using System; using System.Windows.Forms; using Eplan.EplApi.ApplicationFramework; using Eplan.EplApi.Gui; using Eplan.EplApi.DataModel; using Eplan.EplApi.HEServices; namespace SimpleAddIn { public class AddInModule : IEplAddIn { public bool OnRegister( ref bool bLoadOnStart ) { bLoadOnStart = true; return true; } public bool OnUnregister() { return true; } public bool OnInit() { return true; } public bool OnInitGui() { Eplan.EplApi.Gui.Menu oMenu = new Eplan.EplApi.Gui.Menu(); uint menuID = oMenu.AddMainMenu( "eplaner", Eplan.EplApi.Gui.Menu.MainMenuName.eMainMenuHelp, " ", "showWndAction", "   ", 0 /*inserts before*/ ); return true; } public bool OnExit() { return true; } } public class Action_Test : IEplAction { public Action_Test() { frm = new System.Windows.Forms.Form(); rtbox = new System.Windows.Forms.RichTextBox(); rtbox.Dock = System.Windows.Forms.DockStyle.Fill; rtbox.Location = new System.Drawing.Point( 0, 0 ); frm.Controls.Add( rtbox ); } public bool OnRegister( ref string Name, ref int Ordinal ) { Name = "showWndAction"; Ordinal = 20; return true; } public bool Execute( ActionCallingContext oActionCallingContext ) { SelectionSet set = new SelectionSet(); Page[] pages = set.OpenedPages; rtbox.Clear(); foreach ( Page pg in pages ) { rtbox.AppendText( pg.Name + "\n" ); } frm.ShowDialog(); return true; } public void GetActionProperties( ref ActionProperties actionProperties ) { } System.Windows.Forms.Form frm; System.Windows.Forms.RichTextBox rtbox; } } 

Here, the simplest addition is implemented. During the execution of an action (Action Action_Test), we fill in the text field with the necessary information and display the form in a dialog mode. The results are shown in the figures below.





As you can see, the window is displayed on top of the Eplan environment, the work in which is possible only after the window is closed. It is very uncomfortable. Consider the second option additions, displaying information in a normal separate window. To do this, replace the string frm.ShowDialog () with frm.Show () in the Execute method for the Action_Test class. Now you can switch between the add-on window and the environment. But for correct operation after closing the add-on window, you need to upgrade the code of the Action_Test class:

 public class Action_Test : IEplAction { public Action_Test() { frm = new System.Windows.Forms.Form(); rtbox = new System.Windows.Forms.RichTextBox(); rtbox.Dock = System.Windows.Forms.DockStyle.Fill; rtbox.Location = new System.Drawing.Point( 0, 0 ); frm.Controls.Add( rtbox ); frm.FormClosing += FormClosing; } public bool OnRegister( ref string Name, ref int Ordinal ) { Name = "showWndAction"; Ordinal = 20; return true; } private void FormClosing( object sender, FormClosingEventArgs e ) { e.Cancel = true; ( sender as System.Windows.Forms.Form ).Hide(); } public bool Execute( ActionCallingContext oActionCallingContext ) { SelectionSet set = new SelectionSet(); Page[] pages = set.OpenedPages; rtbox.Clear(); foreach ( Page pg in pages ) { rtbox.AppendText( pg.Name + "\n" ); } frm.Show(); return true; } public void GetActionProperties( ref ActionProperties actionProperties ) { } System.Windows.Forms.Form frm; System.Windows.Forms.RichTextBox rtbox; } 

As you can see, it was necessary to add the correct window closing processing (the FormClosing method). Such a solution can be used, but still it is not the most user-friendly. In order for the form to behave as built-in, we will place it on some standard environment window, having previously hidden its contents. To implement this window functionality, we will use WinApi functions. The add-on code is below. The operation algorithm is described in detail in the comments of the ShowDlg () method of the EplanWindowWrapper class.

Main add-on program file, main.cs.

 //@file main.cs //@brief ,  . // @par  : // @$Rev: 1 $.\n // @$Author: idimm $.\n // @$Date:: 2015-12-03 12:05:13#$. using System; using System.Windows.Forms; using Eplan.EplApi.ApplicationFramework; using Eplan.EplApi.Gui; using Eplan.EplApi.DataModel; using Eplan.EplApi.HEServices; namespace SimpleAddIn { public class AddInModule : IEplAddIn { public bool OnRegister( ref bool bLoadOnStart ) { bLoadOnStart = true; return true; } public bool OnUnregister() { return true; } public bool OnInit() { return true; } public bool OnInitGui() { Eplan.EplApi.Gui.Menu oMenu = new Eplan.EplApi.Gui.Menu(); uint menuID = oMenu.AddMainMenu( "eplaner", Eplan.EplApi.Gui.Menu.MainMenuName.eMainMenuHelp, " ", "showWndAction", "   ", 0 /*inserts before*/ ); return true; } public bool OnExit() { return true; } } public class Action_Test : IEplAction { public Action_Test() { rtbox = new System.Windows.Forms.RichTextBox(); rtbox.Dock = System.Windows.Forms.DockStyle.Fill; rtbox.Location = new System.Drawing.Point( 0, 0 ); } public bool OnRegister( ref string Name, ref int Ordinal ) { Name = "showWndAction"; Ordinal = 20; return true; } private void FormClosing( object sender, FormClosingEventArgs e ) { e.Cancel = true; ( sender as System.Windows.Forms.Form ).Hide(); } public bool Execute( ActionCallingContext oActionCallingContext ) { if ( eww == null ) { eww = new EplanWindowWrapper( rtbox, new ToolStrip(), "" ); } if ( !wasInit ) { SelectionSet set = new SelectionSet(); Page[] pages = set.OpenedPages; rtbox.Clear(); foreach ( Page pg in pages ) { rtbox.AppendText( pg.Name + "\n" ); } } eww.ShowDlg(); return true; } public void GetActionProperties( ref ActionProperties actionProperties ) { } System.Windows.Forms.RichTextBox rtbox; EplanWindowWrapper eww; bool wasInit = false; } } 

Additional program module, EplanWindowWrapper.cs.

Expand
 ///@file EplanWindowWrapper.cs ///@brief ,   ,   ///      Eplan. /// /// @par  : /// @$Rev: 1$.\n /// @$Author: idimm$.\n /// @$Date:: 2012-04-07 16:45:35#$. /// using System; using System.Windows.Forms; using System.Runtime.InteropServices; /// <summary> /// ,   Eplan'       . /// </summary> public class EplanWindowWrapper { private IntPtr wndHandle = IntPtr.Zero; /// . private IntPtr dialogHandle = IntPtr.Zero; /// . private IntPtr panelPtr; /// . IntPtr oldDialogWndProc = IntPtr.Zero; ///  . IntPtr oldPanelProc = IntPtr.Zero; ///  . pi.Win32WndProc newWndProc; ///  . Control mainCntrl; /// . byte[] newCaption; ///  . string newCaptionStr; private string windowName; private int panelDlgItemId; private int dialogDlgItemId; private int panelToHideDlgItemId; private int menuClickId; /// <summary> ///        ///   Eplan'. /// </summary> private IntPtr DialogWndProc( IntPtr hWnd, int msg, int wPar, IntPtr lPar ) { if ( hWnd == panelPtr ) { switch ( ( pi.WM ) msg ) { case pi.WM.MOVE: case pi.WM.SIZE: IntPtr dialogPtr = pi.GetParent( mainCntrl.Handle ); pi.RECT rctDialog; pi.RECT rctPanel; pi.GetWindowRect( dialogPtr, out rctDialog ); pi.GetWindowRect( panelPtr, out rctPanel ); int w = rctDialog.Right - rctDialog.Left; int h = rctDialog.Bottom - rctDialog.Top; int dx = rctPanel.Left - rctDialog.Left; int dy = rctPanel.Top - rctDialog.Top; mainCntrl.Location = new System.Drawing.Point( dx, dy ); mainCntrl.Width = w - dx; mainCntrl.Height = h - dy; break; } return pi.CallWindowProc( oldPanelProc, panelPtr, msg, wPar, lPar ); } if ( hWnd == dialogHandle ) { switch ( ( pi.WM ) msg ) { case pi.WM.GETTEXTLENGTH: return ( IntPtr ) newCaption.Length; case pi.WM.SETTEXT: return IntPtr.Zero; case pi.WM.DESTROY: dialogHandle = IntPtr.Zero; pi.SetParent( mainCntrl.Handle, IntPtr.Zero ); mainCntrl.Hide(); System.Threading.Thread.Sleep( 1 ); break; case pi.WM.GETTEXT: System.Runtime.InteropServices.Marshal.Copy( newCaption, 0, lPar, newCaption.Length ); return ( IntPtr ) newCaption.Length; } } return pi.CallWindowProc( oldDialogWndProc, hWnd, msg, wPar, lPar ); } /// <summary> ///    ,    ///   . /// </summary> /// <param name="mainCntrl"> ///   ,   . /// </param> /// <param name="caption"> ///   . /// </param> /// <param name="windowName"> ///   (-),      /// . /// </param> /// <param name="panelDlgItemId"> ///  - -. /// </param> /// <param name="dialogDlgItemId"> ///    -. /// </param> /// <param name="panelToHideDlgItemId"> ///      Eplan,   /// . /// </param> /// <param name="menuClickId"> ///      -. /// </param> public EplanWindowWrapper( Control mainCntrl, string caption, string windowName = " ", int panelDlgItemId = 0xE81F, int dialogDlgItemId = 0x3458, int panelToHideDlgItemId = 0xBC2, int menuClickId = 35506 ) { this.mainCntrl = mainCntrl; mainCntrl.Hide(); newCaptionStr = caption; newCaption = System.Text.Encoding.GetEncoding( 1251 ).GetBytes( caption + '\0' ); this.windowName = windowName; this.panelDlgItemId = panelDlgItemId; this.dialogDlgItemId = dialogDlgItemId; this.panelToHideDlgItemId = panelToHideDlgItemId; this.menuClickId = menuClickId; newWndProc = DialogWndProc; } /// <summary> ///     . /// ///        ,   /// : /// 1   (  " ",   ///  -> ). /// 1.1   :  FindWindowByCaption , ///       DlgItemId (0xE81F -  , /// 0x3458 - ).   ,    4,   1.2. /// 1.2   :  GetDlgItem   ///   (GetChildWindows)  Eplan  DlgItemId /// (0x3458 - ). ///   ,    4,   2. /// 2     (  -> ) ///   . /// 3    (1.1  1.2).      ///   ,  ,   4. /// 4      Eplan' /// (GetDlgItem, 0xBC2 -  , ShowWindow). /// 5.       (SetParent)   ///    . /// 6.     (   ///  ,   ). /// </summary> public void ShowDlg() { if ( mainCntrl.Visible ) { return; } bool isDocked = false; System.Diagnostics.Process oCurrent = System.Diagnostics.Process.GetCurrentProcess(); IntPtr res = pi.FindWindowByCaption( IntPtr.Zero, windowName );//1.1 if ( res != IntPtr.Zero ) { res = pi.GetDlgItem( res, panelDlgItemId ); wndHandle = pi.GetParent( res ); dialogHandle = pi.GetDlgItem( res, dialogDlgItemId ); } else //1.2 { System.Collections.Generic.List<IntPtr> resW = pi.GetChildWindows( oCurrent.MainWindowHandle ); foreach ( IntPtr panel in resW ) { dialogHandle = pi.GetDlgItem( panel, dialogDlgItemId ); if ( dialogHandle != IntPtr.Zero ) { isDocked = true; res = dialogHandle; break; } } if ( res == IntPtr.Zero ) { pi.SendMessage( oCurrent.MainWindowHandle, ( uint ) pi.WM.COMMAND, menuClickId, 0 ); //2 res = pi.FindWindowByCaption( IntPtr.Zero, windowName );//3 if ( res != IntPtr.Zero ) { res = pi.GetDlgItem( res, panelDlgItemId ); wndHandle = pi.GetParent( res ); dialogHandle = pi.GetDlgItem( res, dialogDlgItemId ); } else { resW = pi.GetChildWindows( oCurrent.MainWindowHandle ); foreach ( IntPtr panel in resW ) { dialogHandle = pi.GetDlgItem( panel, dialogDlgItemId ); if ( dialogHandle != IntPtr.Zero ) { isDocked = true; break; } } if ( dialogHandle == IntPtr.Zero ) { System.Windows.Forms.MessageBox.Show( "   !" ); return; } } } } panelPtr = pi.GetDlgItem( dialogHandle, panelToHideDlgItemId ); //4 if ( panelPtr == IntPtr.Zero ) { System.Windows.Forms.MessageBox.Show( "   !" ); return; } pi.ShowWindow( panelPtr, 0 ); pi.SetParent( mainCntrl.Handle, dialogHandle ); //5 mainCntrl.Show(); int dy = 0; if ( isDocked ) { dy = 17; } pi.RECT dialogRect; pi.GetWindowRect( dialogHandle, out dialogRect ); mainCntrl.Location = new System.Drawing.Point( 0, dy ); int w = dialogRect.Right - dialogRect.Left; int h = dialogRect.Bottom - dialogRect.Top - dy; mainCntrl.Width = w; mainCntrl.Height = h; oldDialogWndProc = pi.SetWindowLong( dialogHandle, pi.GWL_WNDPROC, newWndProc ); oldPanelProc = pi.SetWindowLong( panelPtr, pi.GWL_WNDPROC, newWndProc ); pi.SetWindowText( dialogHandle, newCaptionStr ); pi.SetWindowText( wndHandle, newCaptionStr ); } } /// <summary> /// Platform Invoke functions. /// </summary> public class pi { /// <summary> /// Window procedure. /// </summary> public delegate IntPtr Win32WndProc( IntPtr hWnd, int msg, int wParam, IntPtr lParam ); public const int GWL_WNDPROC = -4; /// <summary> /// Changes an attribute of the specified window. The function also sets /// the 32-bit (long) value at the specified offset into the extra window /// memory. /// </summary> /// <param name="hWnd"> /// A handle to the window and, indirectly, the class to which the window /// belongs. /// </param> /// <param name="nIndex">The zero-based offset to the value to be set. /// Valid values are in the range zero through the number of bytes of /// extra window memory, minus the size of an integer. To set any other /// value, specify one of the following values: GWL_EXSTYLE, /// GWL_HINSTANCE, GWL_ID, GWL_STYLE, GWL_USERDATA, GWL_WNDPROC /// </param> /// <param name="dwNewLong">The replacement value.</param> /// <returns> /// If the function succeeds, the return value is the previous value of /// the specified 32-bit integer. If the function fails, the return value /// is zero. To get extended error information, call GetLastError. /// </returns> [DllImport( "user32" )] public static extern IntPtr SetWindowLong( IntPtr hWnd, int nIndex, Win32WndProc newProc ); /// <summary> /// Set window text. /// </summary> [DllImport( "user32.dll", SetLastError = true, CharSet = CharSet.Auto )] public static extern bool SetWindowText( IntPtr hwnd, String lpString ); /// <summary> /// Find window by Caption only. Note you must pass IntPtr. /// Zero as the first parameter. /// </summary> [DllImport( "user32.dll", EntryPoint = "FindWindow", SetLastError = true )] public static extern System.IntPtr FindWindowByCaption( IntPtr ZeroOnly, string lpWindowName ); /// <summary> /// Windows Messages /// Defined in winuser.h from Windows SDK v6.1 /// Documentation pulled from MSDN. /// </summary> public enum WM : uint { /// <summary> /// The WM_NULL message performs no operation. An application sends /// the WM_NULL message if it wants to post a message that the /// recipient window will ignore. /// </summary> NULL = 0x0000, /// <summary> /// The WM_CREATE message is sent when an application requests that /// a window be created by calling the CreateWindowEx or CreateWindow /// function. (The message is sent before the function returns.) /// The window procedure of the new window receives this message /// after the window is created, but before the window becomes visible. /// </summary> CREATE = 0x0001, /// <summary> /// The WM_DESTROY message is sent when a window is being destroyed. /// It is sent to the window procedure of the window being destroyed /// after the window is removed from the screen. /// This message is sent first to the window being destroyed and then /// to the child windows (if any) as they are destroyed. During the /// processing of the message, it can be assumed that all child /// windows still exist. /// /// </summary> DESTROY = 0x0002, /// <summary> /// The WM_MOVE message is sent after a window has been moved. /// </summary> MOVE = 0x0003, /// <summary> /// The WM_SIZE message is sent to a window after its size has changed. /// </summary> SIZE = 0x0005, //... /// <summary> /// An application sends a WM_SETTEXT message to set the text of a /// window. /// </summary> SETTEXT = 0x000C, /// <summary> /// An application sends a WM_GETTEXT message to copy the text that /// corresponds to a window into a buffer provided by the caller. /// </summary> GETTEXT = 0x000D, /// <summary> /// An application sends a WM_GETTEXTLENGTH message to determine the /// length, in characters, of the text associated with a window. /// </summary> GETTEXTLENGTH = 0x000E, //... /// <summary> /// The WM_COMMAND message is sent when the user selects a command /// item from a menu, when a control sends a notification message to /// its parent window, or when an accelerator keystroke is translated. /// </summary> COMMAND = 0x0111, } /// <summary> /// Get window parent. /// </summary> [DllImport( "user32.dll", ExactSpelling = true, CharSet = CharSet.Auto )] public static extern IntPtr GetParent( IntPtr hWnd ); /// <summary> /// Set window parent. /// </summary> [DllImport( "user32.dll", SetLastError = true )] public static extern IntPtr SetParent( IntPtr hWndChild, IntPtr hWndNewParent ); /// <summary> /// Windows rectangle structure. /// </summary> [StructLayout( LayoutKind.Sequential )] public struct RECT { public int Left; // x position of upper-left corner public int Top; // y position of upper-left corner public int Right; // x position of lower-right corner public int Bottom; // y position of lower-right corner } /// <summary> /// Get window rectangle. /// </summary> [DllImport( "user32.dll" )] [return: MarshalAs( UnmanagedType.Bool )] public static extern bool GetWindowRect( IntPtr hWnd, out RECT lpRect ); /// <summary> /// Calls window procedure. /// </summary> [DllImport( "user32.dll" )] public static extern IntPtr CallWindowProc( IntPtr lpPrevWndFunc, IntPtr hWnd, int Msg, int wParam, IntPtr lParam ); /// <summary> /// Gets dialog item by its ID. /// </summary> [DllImport( "user32.dll" )] public static extern IntPtr GetDlgItem( IntPtr hDlg, int nIDDlgItem ); /// <summary> /// Delegate for the EnumChildWindows method /// </summary> /// <param name="hWnd">Window handle</param> /// <param name="parameter"> /// Caller-defined variable; we use it for a pointer to our list /// </param> /// <returns>True to continue enumerating, false to bail.</returns> public delegate bool EnumWindowProc( IntPtr hWnd, IntPtr parameter ); /// <summary> /// Returns a list of child windows /// </summary> /// <param name="parent">Parent of the windows to return</param> /// <returns>List of child windows</returns> public static System.Collections.Generic.List<IntPtr> GetChildWindows( IntPtr parent ) { System.Collections.Generic.List<IntPtr> result = new System.Collections.Generic.List<IntPtr>(); GCHandle listHandle = GCHandle.Alloc( result ); try { EnumWindowProc childProc = new EnumWindowProc( EnumWindow ); EnumChildWindows( parent, childProc, GCHandle.ToIntPtr( listHandle ) ); } finally { if ( listHandle.IsAllocated ) listHandle.Free(); } return result; } /// <summary> /// Callback method to be used when enumerating windows. /// </summary> /// <param name="handle">Handle of the next window</param> /// <param name="pointer"> /// Pointer to a GCHandle that holds a reference to the list to fill /// </param> /// <returns>True to continue the enumeration, false to bail</returns> public static bool EnumWindow( IntPtr handle, IntPtr pointer ) { System.Runtime.InteropServices.GCHandle gch = System.Runtime.InteropServices.GCHandle.FromIntPtr( pointer ); System.Collections.Generic.List<IntPtr> list = gch.Target as System.Collections.Generic.List<IntPtr>; if ( list == null ) { throw new InvalidCastException( "GCHandle Target could not be cast as List<IntPtr>" ); } list.Add( handle ); return true; } [DllImport( "user32" )] [return: MarshalAs( UnmanagedType.Bool )] public static extern bool EnumChildWindows( IntPtr window, EnumWindowProc callback, IntPtr i ); [DllImport( "user32.dll" )] public static extern IntPtr SendMessage( IntPtr hWnd, UInt32 Msg, Int32 wParam, Int32 lParam ); [DllImport( "user32.dll" )] [return: MarshalAs( UnmanagedType.Bool )] public static extern bool ShowWindow( IntPtr hWnd, Int32 nCmdShow ); } 


The main functionality is implemented in the EplanWindowWrapper class. When creating an instance of a class, you need to specify everything necessary to correctly replace the contents of the standard window. To obtain the necessary identifiers, you can use the Spy ++ utility, which is included with Visual Studio. For convenience, all the necessary WinApi function wrappers (Platform Invoke) are in a separate class pi.

The result of this implementation is given below.



We see that the window now behaves just like any other standard environment window, but contains user information, and the automation engineer is as comfortable as possible to work with him now.

In this way, you can add in principle any functionality for an application if its API for developing an add-on has limited functionality.

Source: https://habr.com/ru/post/272805/


All Articles