C#でのアドオン(アドイン)の標準開発への非標準アプローチ

アドインの助けを借りて、アプリケーションの毎日の使用を大幅に促進する追加機能を実装できます。 典型的なアプローチは簡単です-アドオンの開発のためのアプリケーションAPIを研究し、それを別個のライブラリとして実装します。 通常、複数の言語から選択できますが、C#は、.NetプラットフォームでWindows用のアプリケーションを開発するための普及率と人気のために、このリストに載る可能性が最も高いでしょう。



ただし、多くの場合、アドオンでは、アプリケーションの使用時に必要な何らかの視覚情報を常に表示する必要があります。 この問題の簡単な解決策-カスタムウィンドウを作成し、それをすべてのウィンドウの上に表示するか、フォアグラウンドに移動する-では、ユーザースペースの編成に不便が生じます。 どうにかしてそれらに耐えることができますが、そのようなウィンドウが複数ある場合はどうでしょうか?



原則として、アプリケーション環境自体では、すべてが必要に応じて編成されます-必要なアクションは、追加のウィンドウ、ユーザーが自分で設定したい場所とサイズの形で実装されます。 APIには、カスタムウィンドウを環境に統合するためのこの機能が含まれている必要があると想定できますが、奇妙なことに、そのような機能はおそらく存在しないでしょう。 ただし、本当に必要な場合は実装できます。



Windows用アプリケーションの動作の基本はイベントモデルです。アプリケーションは(ユーザーとオペレーティングシステムの両方から)メッセージを受信し、それらに応答します。たとえば、コンテンツを描画します。 この機能はすべてウィンドウプロシージャに実装されているため、このプロシージャを独自のプロシージャに置き換えて、ウィンドウ機能アルゴリズムを完全に変更できます。 このような低レベルのタスクを実行するには、WinAPIというオペレーティングシステムの機能を有効にする必要があります。 C#(.NET Framework)とWinAPI間の接続は、プラットフォーム呼び出しサービス(マネージコード(C#)からアンマネージWinAPI(C)プロシージャコードを呼び出す機能)を使用して実装されます。



たとえば、プロのCADシステムEplanのアドオンの開発を検討してください。 典型的なアドオンの開発経験が既にあると想定されている(そうでない場合は、 ここで基本を理解することができます )ので、視覚的なアドオンウィンドウの実装に関連するアクションのみがコメントされます。 まず、現在開いているページのリストを(フォアグラウンドで)別のウィンドウに表示するアドオンオプションを検討してください。



メインのアドオンプログラムファイル、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; } }
      
      





ここで最も簡単な追加が実装されます。アクション(Action Action_Test)の実行中に、テキストフィールドに必要な情報を入力し、フォームをダイアログモードで表示します。 結果を以下の図に示します。











ご覧のとおり、ウィンドウはEplan環境の上部に表示されます。この作業は、ウィンドウを閉じた後にのみ可能です。 これは非常に不便です。 通常の別のウィンドウに情報を表示するアドオンの2番目のバージョンを検討してください。 これを行うには、Action_TestクラスのExecuteメソッドで、文字列frm.ShowDialog()をfrm.Show()に置き換えます。 これで、アドオンウィンドウと環境を切り替えることができます。 ただし、アドオンウィンドウを閉じた後の正しい操作のためには、Action_Testクラスのコードを最新化する必要があります。



 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; }
      
      





どうやら、正しいウィンドウを閉じる処理(FormClosingメソッド)を追加する必要があったようです。 このようなソリューションは使用できますが、それでもユーザーにとって最も便利ではありません。 フォームが組み込みのフォームとして動作するようにするには、フォームを標準の環境ウィンドウに配置し、そのコンテンツを最初に非表示にします。 このウィンドウ機能を実装するには、WinApi関数を使用します。 アドオンコードを以下に示します。 操作アルゴリズムの詳細は、EplanWindowWrapperクラスのShowDlg()メソッドのコメントで説明されています。



メインのアドオンプログラムファイル、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; } }
      
      





追加のソフトウェアモジュール、EplanWindowWrapper.cs。



展開する
 ///@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 ); }
      
      







主な機能は、EplanWindowWrapperクラスに実装されています。 クラスのインスタンスを作成するときは、標準ウィンドウのコンテンツを正しく置き換えるために必要なすべてを指定する必要があります。 必要な識別子を取得するには、Visual Studioの一部であるSpy ++ユーティリティを使用できます。 便宜上、WinApi関数(Platform Invoke)の必要なラッパーはすべて、個別のpiクラスに配置されます。



この実装の結果を以下に示します。







これで、ウィンドウは他の標準環境ウィンドウと同じように動作しますが、ユーザー情報が含まれ、自動化エンジニアが最も快適に作業できるようになりました。



このように、アドオンの開発用のAPIの機能が制限されている場合、原則として、アプリケーションに任意の機能を追加できます。



All Articles