С помощью Add-In'a можно реализовать дополнительную функциональность, серьезно облегчающую повседневное использование приложения. Типовой подход прост – изучаем API приложения для разработки дополнения и реализуем его в виде отдельной библиотеки. На выбор обычно предлагается несколько языков, С# скорее всего будет в данном списке из-за его распространенности и популярности для разработки приложений под Windows на платформе .Net.

Однако часто бывает необходимо, чтобы дополнение постоянно отображало какую-то визуальную информации, нужную при использовании приложения. При простом решении данной проблемы — создание пользовательского окна и отображении его либо поверх всех окон, либо перемещением его на передний план — мы получаем некоторые неудобства с организацией пользовательского пространства. С ними можно кое-как мириться, но что делать, если таких окон должно быть несколько?

Как правило в самой среде приложения все организовано как надо — нужные действия реализованы в виде дополнительных окон, расположение и размер которых пользователь как хочет, так и настраивает для себя. Можно предположить, что API должен содержать данную функциональность интеграции пользовательских окон в среду, но как ни странно, скорее всего такой функционал будет отсутствовать. Но, если очень нужно, это можно реализовать.

В основе работы любого приложения под Windows лежит событийная модель – приложение получает сообщения (как от пользователя, так и от операционной системы) и реагирует на них, например, отрисовывает свое содержимое. Вся данная функциональность реализована в оконной процедуре, таким образом, заменив данную процедуру своей пользовательской, мы можем полностью изменить алгоритм функционирования окна. Для выполнения такой низкоуровневой задачи необходимо обратиться к функциям операционной системы – WinAPI. Связь C# (.NET Framework) и WinAPI реализована с помощью Platform Invoke сервисов – возможности из управляемого кода (C#) вызывать неуправляемый код процедур WinAPI (С).

Рассмотрим на примере разработку дополнения для профессиональной САПР 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, работа в котором возможна только после закрытия окна. Это очень неудобно. Рассмотрим второй вариант дополнения, отображающего информацию в обычном отдельном окне. Для этого заменим строку frm.ShowDialog() на frm.Show() в методе Execute для класса Action_Test. Теперь можно переключаться между окном дополнения и средой. Но для корректной работы после закрытия окна дополнения необходимо модернизировать код класса 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. Код дополнения приведен ниже. Алгоритм работы подробно расписан в комментариях метода ShowDlg() класса EplanWindowWrapper.

Основной программный файл дополнения, 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. При создании экземпляра класса необходимо указать все необходимое для корректной замены содержимого стандартного окна. Для получения необходимых идентификаторов можно использовать утилиту Spy++, которая входит в состав Visual Studio. Для удобства, все необходимые обертки WinApi функций (Platform Invoke) вынесены в отдельный класс pi.

Результат работы такой реализации приведен ниже.



Видим, что теперь окно ведет себя также, как и любое другое стандартное окно среды, но содержит пользовательскую информации, и инженеру по автоматизации максимально удобно с ним теперь работать.

Таким подобным образом можно добавить в принципе любую функциональность для приложения, если его API для разработки дополнения имеет ограниченный функционал.

Комментарии (2)


  1. QtRoS
    10.12.2015 23:47

    Несколько дотошных замечаний — lParam и wParam у Вашей оконной процедуры лучше бы сделать чем-то более похожим, а то IntPtr платформозависимый, а Int нет, хотя они в принципе различаются только знаковостью. Далее, при явном указании названия функции, например:

    [DllImport( "user32.dll", EntryPoint = "FindWindow", SetLastError = true )]
    

    лучше сразу писать
    [DllImport( "user32.dll", EntryPoint = "FindWindowW", SetLastError = true )]
    

    то бишь явно указывать юникодную версию, чтобы маршаллеру лишних движений по поиску внутри не делать.


    1. idimm
      11.12.2015 11:49

      С замечаниями согласен — обрамления API-функций особо не шлифовал, так как на этапе отладки использовал много таковых, а для рабочей версии оставил только необходимые.