home *** CD-ROM | disk | FTP | other *** search
Wrap
// Pavel Zolnikov[http://www.codeproject.com/script/profile/whos_who.asp?id=35980], 2002 using System; using System.Drawing; using System.Threading; using System.Collections; using System.Diagnostics; using System.Windows.Forms; using System.ComponentModel; using System.Drawing.Drawing2D; using System.Security.Permissions; using System.Runtime.InteropServices; using System.Runtime.CompilerServices; namespace ZCommon { /// <summary> /// Custom control that incapsulates command prompt window handling /// </summary> public class ConsoleCtrl : UserControl { /// <summary> /// Control that hosts command prompt window. /// </summary> /// <remarks> /// Putting command prompt window inside clippingPanel makes it easy to clip unwanted user elements /// such as frame borders and caption bar. /// </remarks> Panel clippingPanel; ForegroundWatcher foregroundWatcher = new ForegroundWatcher(); static Process consoleProcess = null; static ArrayList consoleControls = new ArrayList(); static readonly object syncRoot = typeof(ConsoleCtrl); public ConsoleCtrl() { SetStyle( ControlStyles.AllPaintingInWmPaint, true ); SetStyle( ControlStyles.UserPaint, true ); SetStyle( ControlStyles.Selectable, true ); SetStyle( ControlStyles.ContainerControl, true ); clippingPanel = new Panel(); clippingPanel.Parent = this; //position and size clippingPanel according to margins Rectangle bounds = ClientRectangle; bounds.Inflate( -consoleMargin.Width , -consoleMargin.Width ); clippingPanel.Bounds = bounds; //need to handle these events to resize commandPrompt window accordingly this.SizeChanged += new EventHandler( ControlSizeChanged ); this.Move += new EventHandler( ControlSizeChanged ); this.LocationChanged += new EventHandler( ControlSizeChanged ); this.Resize += new EventHandler( ControlSizeChanged ); lock(syncRoot) { consoleControls.Add(this); if( consoleControls.Count == 1 ) { OpenConsole(); } } ConsoleOwnerChanged += new EventHandler( ConsoleOwner_Changed ); foregroundWatcher.ConsoleGotFocus += new EventHandler( ConsoleFocused_Changed ); foregroundWatcher.ConsoleLostFocus += new EventHandler( ConsoleFocused_Changed ); } protected override void Dispose(bool disposing) { base.Dispose(disposing); lock(syncRoot) { consoleControls.Remove(this); if( consoleControls.Count == 0 ) { ConsoleOwner = null; CloseConsole(); } else ConsoleOwner = (ConsoleCtrl)consoleControls[0]; } } public IntPtr MainWindowHandle { set { Debug.Assert( Win32.IsWindow(value) ); mainWindowHandle = value; foregroundWatcher.Start( mainWindowHandle ); } get { return mainWindowHandle; } }IntPtr mainWindowHandle; /// <summary> /// Handle to the main commandPrompt window. /// </summary> static public IntPtr ConsoleWindowHandle { get { return consoleWindowHandle; } }static IntPtr consoleWindowHandle; static public event EventHandler ConsoleLostFocus; static public event EventHandler ConsoleGotFocus; static public bool ConsoleFocused { get { return ConsoleOwner!=null && ConsoleOwner.foregroundWatcher.ConsoleFocused; } } static void SetScreenBufferSize(Int16 size) { IntPtr handleOut = Win32.GetStdHandle((Int32)Win32.STD_HANDLE.OUTPUT); Win32.CONSOLE_SCREEN_BUFFER_INFO csbi; Win32.GetConsoleScreenBufferInfo( handleOut, out csbi ); Win32.COORD newSize = csbi.dwSize; short windowWidth = (short) (csbi.srWindow.Right - csbi.srWindow.Left); newSize.X = size > windowWidth ? size : windowWidth; if( ScreenBufferSize == newSize.X ) return; Win32.SetConsoleScreenBufferSize(handleOut, newSize); } static public Int16 ScreenBufferSize { get { IntPtr handleOut = Win32.GetStdHandle((Int32)Win32.STD_HANDLE.OUTPUT); Win32.CONSOLE_SCREEN_BUFFER_INFO csbi; Win32.GetConsoleScreenBufferInfo( handleOut, out csbi ); return csbi.dwSize.X; } set { SetScreenBufferSize(value); if( ConsoleOwner != null ) { ConsoleOwner.ResizeConsole(); ConsoleOwner.Invalidate(); } } } static Size ConosleVisibleNonClientSize { get { Win32.WINDOWINFO wi; Win32.GetWindowInfo(ConsoleWindowHandle, out wi);//retreives prompt window info //calculates size that is hidden to the left and up of prompt client area Size szConsoleHidden = new Size( wi.rcClient.left - wi.rcWindow.left, wi.rcClient.top - wi.rcWindow.top ); Rectangle windowRect = Rectangle.FromLTRB( wi.rcWindow.left, wi.rcWindow.top, wi.rcWindow.right, wi.rcWindow.bottom ); Rectangle clientRect = Rectangle.FromLTRB( wi.rcClient.left, wi.rcClient.top, wi.rcClient.right, wi.rcClient.bottom ); return windowRect.Size - clientRect.Size + szConsoleHidden; } } public bool ScreenBufferAutoGrow { get { return screenBufferAutoGrow; } set { screenBufferAutoGrow = value; OnSizeChanged(EventArgs.Empty); } } bool screenBufferAutoGrow = true; public Int16 FittingScreenBufferSize { get { return (Int16)( (ConsoleAreaSize.Width-ConosleVisibleNonClientSize.Width) / FontSize.Width - 1); } } /// <summary> /// Copy of commandPrompt window's system menu. /// </summary> /// <remarks> /// When command commandPrompt window is created we remove unwanted menu items from it such as Move, Resize etc. /// The rest of menu with all Copy, Paste and Properties commands can be accessed through this property. /// </remarks> static public ContextMenu SystemMenu { get { lock(syncRoot) { if( systemMenu == null ) { systemMenu =new ContextMenu(); IntPtr hMenu = Win32.GetSystemMenu( ConsoleWindowHandle, false ); BuildMenu( hMenu, systemMenu ); } return systemMenu; } } }static ContextMenu systemMenu; /// <summary> /// Emulates pressing Esc key so all previously typed symbols are cleared, selection operation canceled etc. /// </summary> static public void SendEscKey() { Win32.SendMessage( ConsoleWindowHandle, Win32.WM_KEYDOWN, 0x1B, 1 ); } /// <summary> /// Sends string to commandPrompt as if it was typed from keyboard. /// </summary> /// <param name="text"></param> /// <param name="showText">Controls whether this string will be visible after command execute.</param> /// <param name="execute">Controls whether to press Enter at the end making command execute.</param> static public void Output(String text, bool showText, bool execute) { Debug.Assert( showText | execute );//either show output string or do execute lock(syncRoot) { Win32.COORD pos = new Win32.COORD(); if( !showText ) pos = RememberPosition(); foreach(char c in text) { OutputChar( c ); } if( execute ) OutputChar( '\r' ); if( !showText ) RestorePosition( pos, true ); } } /// <summary> /// Calculates size of the commandPrompt font. /// </summary> /// <remarks> /// Unfortunately I was unable to get exact information so it is only approximate. /// The reason is that GetCurrentConsoleFont requires handle to screen buffer and there is no /// documented way to get current screen buffer of the commandPrompt ( no pair API to SetConsoleActiveScreenBuffer ). /// I have this hiddenBuffer handle to screen buffer I create myself. It works fine for the default font size, /// but if font was changed it is no good anymore. /// </remarks> static public Size FontSize { get { lock(syncRoot) { if( fontSize.IsEmpty ) { IntPtr hiddenBuffer; //creates hidden commandPrompt screen buffer that we can use to get commandPrompt font size hiddenBuffer = Win32.CreateConsoleScreenBuffer( Win32.DESIRED_ACCESS.GENERIC_READ | Win32.DESIRED_ACCESS.GENERIC_WRITE, Win32.FILE_SHARE.READ | Win32.FILE_SHARE.WRITE, null, Win32.CONSOLE_TEXTMODE_BUFFER, IntPtr.Zero ); Win32.CONSOLE_FONT_INFO cfi; bool b = Win32.GetCurrentConsoleFont( hiddenBuffer, false, out cfi ); Win32.COORD szFont = Win32.GetConsoleFontSize( hiddenBuffer, cfi.nFont); fontSize = new Size( szFont.X, szFont.Y ); Win32.CloseHandle( hiddenBuffer ); } return fontSize; } } }static Size fontSize; /// <summary> /// Changes width of the screen buffer by delta. /// </summary> /// <remarks> /// It appears that cconsole screen buffer can always be increased, but decreased only if windows size is less the bufer size. /// If this function fails to decrease size of the bufer by delta it attemts to decreas just by one. /// This allows user to set size of the bufer that exactly matches size of the window. /// </remarks> static public void ResizeConsoleScreenBuffer( Int16 delta ) { IntPtr hOut = Win32.GetStdHandle((Int32)Win32.STD_HANDLE.OUTPUT); Win32.CONSOLE_SCREEN_BUFFER_INFO csbi; Win32.GetConsoleScreenBufferInfo( hOut, out csbi ); Win32.COORD newSize = csbi.dwSize; newSize.X += delta; if( !Win32.SetConsoleScreenBufferSize(hOut, newSize) && delta<0 ) { int err = Marshal.GetLastWin32Error(); newSize.X += (Int16)(-delta-1); bool b = Win32.SetConsoleScreenBufferSize(hOut, newSize); } if( ConsoleOwner != null ) { ConsoleOwner.ResizeConsole(); ConsoleOwner.Invalidate(); } } /// <summary> /// Raised when cmd.exe consoleProcess exits. Often when user types 'exit' in the command commandPrompt. /// </summary> static public event EventHandler ConsoleExited; static public bool ConsoleRunning { get { return consoleRunning; } }static bool consoleRunning; static bool MyHandlerRoutine(Win32.ConsoleEvents eventId) { if( eventId == Win32.ConsoleEvents.CTRL_C_EVENT || eventId == Win32.ConsoleEvents.CTRL_BREAK_EVENT ) return true;//don't let default handler to call ExitProcess else return false; } static Win32.ConsoleHandlerRoutine MyHandlerRoutineDelegate = new Win32.ConsoleHandlerRoutine(MyHandlerRoutine); /// <summary> /// Starts cmd.exe consoleProcess and gets access to its commandPrompt. /// </summary> /// <param name="dir">Path to the initial file system folder for cmd.exe.</param> static public void OpenConsole() { consoleRunning = true; Win32.AllocConsole(); consoleWindowHandle = Win32.GetConsoleWindow(); Win32.ShowWindow( consoleWindowHandle, 2 ); Win32.SetConsoleCtrlHandler(MyHandlerRoutineDelegate, true );//disables ExitProcess on Ctrl+C and Ctrl+Break //removes not desirable system menu items IntPtr hMenu = Win32.GetSystemMenu( ConsoleWindowHandle, false ); Win32.RemoveMenu( hMenu, 0, 0x400 );//restore Win32.RemoveMenu( hMenu, 0, 0x400 );//move Win32.RemoveMenu( hMenu, 0, 0x400 );//size Win32.RemoveMenu( hMenu, 0, 0x400 );//minimize Win32.RemoveMenu( hMenu, 0, 0x400 );//maximize Win32.RemoveMenu( hMenu, 0, 0x400 );//separator Win32.RemoveMenu( hMenu, 0, 0x400 );//close Win32.STARTUPINFO si = new Win32.STARTUPINFO(); Win32.PROCESS_INFORMATION pi = new Win32.PROCESS_INFORMATION(); bool b = Win32.CreateProcessW( null, "cmd.exe", IntPtr.Zero, IntPtr.Zero, false, 0x0400400, IntPtr.Zero, null, si, pi ); consoleProcess = Process.GetProcessById( pi.dwProcessId ); //watching for cmd.exe consoleProcess exit consoleProcess.EnableRaisingEvents = true; consoleProcess.Exited += new EventHandler( consoleProcess_OnExited ); } /// <summary> /// Ends cmd.exe consoleProcess, closes handles. /// </summary> static public void CloseConsole() { if( !ConsoleRunning ) return; Win32.FreeConsole(); //Process has this MainWindowHandle property. Unfortunately if I use it, explorer.exe can not finish. //explorer.exe consoleProcess just remains runnig with a couple of threads waiting for an Event. //even if MainWindowHandle is accessed indirectly - through Process.CloseMainWindow it has the same effect. //that is why I am not using it for closing commandPrompt consoleProcess. if( ConsoleWindowHandle != IntPtr.Zero ) { //emulates click on 'Close' commandPrompt system menu item Win32.SendMessage( ConsoleWindowHandle, Win32.WM_COMMAND, 0xf060,0 ); } //last resort - if commandPrompt did not close, killing the consoleProcess if( consoleProcess!=null && !consoleProcess.HasExited ) { try { consoleProcess.Kill(); } catch( System.ComponentModel.Win32Exception e) { Debug.WriteLine(e.Message); Debug.WriteLine(e.ToString()); } } } static public ConsoleCtrl ConsoleOwner { get { return consoleOwner; } set { lock(syncRoot) { consoleOwner = value; if( ConsoleOwnerChanged!= null ) ConsoleOwnerChanged(null, EventArgs.Empty); } } }static ConsoleCtrl consoleOwner; static public event EventHandler ConsoleOwnerChanged; void ConsoleOwner_Changed(Object sender, EventArgs ea ) { if( ConsoleOwner != this ) { this.Hide(); } else { //puts prompt window inside clippingPanel Win32.SetParent( ConsoleWindowHandle, clippingPanel.Handle ); } } void ConsoleFocused_Changed(Object sender, EventArgs ea ) { if( ConsoleOwner == this ) { Invalidate(); } if( ConsoleFocused && ConsoleGotFocus != null ) ConsoleGotFocus (this,ea); if( !ConsoleFocused && ConsoleLostFocus != null ) ConsoleLostFocus(this,ea); } void ControlSizeChanged(object sender, EventArgs ea) { if( ConsoleOwner != this ) return; if( ScreenBufferAutoGrow ) { if( FittingScreenBufferSize > ScreenBufferSize ) SetScreenBufferSize( FittingScreenBufferSize ); } ResizeConsole(); Invalidate(); } Size ConsoleAreaSize { get { Rectangle r = ClientRectangle; r.Inflate(-consoleMargin.Width, -consoleMargin.Height); return r.Size; } } /// <summary> /// Resizes prompt window according to the current size of the control. /// </summary> void ResizeConsole() { Win32.WINDOWINFO wi; Win32.GetWindowInfo(ConsoleWindowHandle, out wi);//retreives prompt window info //calculates size that is hidden to the left and up of prompt client area Size szConsoleHidden = new Size( wi.rcClient.left - wi.rcWindow.left, wi.rcClient.top - wi.rcWindow.top ); Size szMaxConsoleClientArea = ConsoleAreaSize - ConosleVisibleNonClientSize; if( fontSize.Width != 0 ) szMaxConsoleClientArea.Width -= (szMaxConsoleClientArea.Width % fontSize.Width); if( fontSize.Height != 0 ) szMaxConsoleClientArea.Height -= (szMaxConsoleClientArea.Height % fontSize.Height); Size sz = szMaxConsoleClientArea + ConosleVisibleNonClientSize + szConsoleHidden; Win32.MoveWindow( ConsoleWindowHandle, - szConsoleHidden.Width, - szConsoleHidden.Height, sz.Width, sz.Height, true); //now we can get actual size of console window Win32.GetWindowInfo(ConsoleWindowHandle, out wi); //resizing clippingPanel so it hides frame borders and caption of prompt clippingPanel.Width = wi.rcWindow.right-wi.rcWindow.left - (int)(wi.cxWindowBorders) - szConsoleHidden.Width - 2; clippingPanel.Height = wi.rcWindow.bottom-wi.rcWindow.top - (int)(wi.cyWindowBorders) - szConsoleHidden.Height - 2; } /// <summary> /// Margins between prompt/clippingPanel and ConsoleCtrl bounds /// </summary> /// <remarks> /// Set margins slightly larger then size of the border we paint around the prompt. /// This makes border nice and visible even if prompt text backround was changed. /// Recalculated every time as we don't want to handle SystemInformation chenged events. /// </remarks> Size consoleMargin { get { return new Size( SystemInformation.Border3DSize.Width<<1, SystemInformation.Border3DSize.Height<<1 ); } } /// <summary> /// Paints border around prompt window. /// </summary> protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { //calculate border rectangle Rectangle border = clippingPanel.ClientRectangle; border = RectangleToClient(clippingPanel.RectangleToScreen(border)); border.Inflate( consoleMargin.Width, consoleMargin.Height ); if( !ConsoleFocused ) //draws border around prompt window ControlPaint.DrawBorder3D( e.Graphics, border, Border3DStyle.Etched, Border3DSide.All ); else ControlPaint.DrawBorder3D( e.Graphics, border, Border3DStyle.Bump, Border3DSide.All ); } protected override void OnVisibleChanged(EventArgs ea) { base.OnVisibleChanged(ea); if( Visible ) { ConsoleOwner = this; ResizeConsole(); } } /// <summary> /// Forwards menu click to the commandPrompt window. (Sends WM_COMMAND with appropriate Id). /// </summary> static void SystemMenuItem_OnClick(Object sender, EventArgs ea) { TagMenuItem mi = (TagMenuItem)sender; Win32.SendMessage( ConsoleWindowHandle, Win32.WM_COMMAND, (Int32)mi.Tag, 0 ); fontSize = Size.Empty;//there is a chance that console font size was changed } /// <summary> /// Recursively copies commandPrompt window's system menu so it can be accessed through SystemMenu property. /// </summary> static void BuildMenu(IntPtr hmenu, Menu menu) { for(Int32 i=0; i<Win32.GetMenuItemCount(hmenu);i++) { Win32.MENUITEMINFO mii = new Win32.MENUITEMINFO(); mii.fMask = Win32.MIIM.STRING | Win32.MIIM.ID | Win32.MIIM.SUBMENU; bool b = Win32.GetMenuItemInfo( hmenu, i, true, mii ); MenuItem item = new TagMenuItem( mii.dwTypeData, mii.wID, new EventHandler(SystemMenuItem_OnClick) ); menu.MenuItems.Add((int)i, item ); if( mii.hSubMenu != IntPtr.Zero ) BuildMenu( mii.hSubMenu, item ); } } /// <summary> /// Sends one character to commandPrompt as if it was typed from keyboard. /// </summary> static void OutputChar(char c) { IntPtr hIn = Win32.GetStdHandle( (Int32)Win32.STD_HANDLE.INPUT ); Int32 cnt; Win32.KEY_INPUT_RECORD r = new Win32.KEY_INPUT_RECORD(); r.bKeyDown = true; r.wRepeatCount = 1; r.wVirtualKeyCode = 0; r.wVirtualScanCode = 0; r.UnicodeChar = c; r.dwControlKeyState = 0; Win32.WriteConsoleInput( hIn ,r, 1, out cnt);//key down event r.bKeyDown = false; Win32.WriteConsoleInput( hIn ,r, 1, out cnt);//key up } /// <summary> /// Current cursor position. /// </summary> static Win32.COORD RememberPosition() { IntPtr hOut = Win32.GetStdHandle((Int32)Win32.STD_HANDLE.OUTPUT); Win32.CONSOLE_SCREEN_BUFFER_INFO csbi; bool b = Win32.GetConsoleScreenBufferInfo( hOut, out csbi ); return csbi.dwCursorPosition; } /// <summary> /// Sets cursor position and erases commandPrompt screen that is after new position if requested. /// </summary> static void RestorePosition(Win32.COORD pos, bool erase) { IntPtr hOut = Win32.GetStdHandle((Int32)Win32.STD_HANDLE.OUTPUT); Win32.CONSOLE_SCREEN_BUFFER_INFO csbi; bool b = Win32.GetConsoleScreenBufferInfo( hOut, out csbi ); b = Win32.SetConsoleCursorPosition( hOut, pos ); if( erase ) { Int32 written; //calculates number of characters we need to clean Int32 num = (csbi.dwSize.X) * (csbi.dwCursorPosition.Y - pos.Y ) + pos.X - csbi.dwCursorPosition.X; //writes spaces to commandPrompt b = Win32.WriteConsole( hOut, new String(' ',num), num, out written, IntPtr.Zero ); //have to set position again b = Win32.SetConsoleCursorPosition( hOut, pos ); } } /// <summary> /// Complements Exited event. /// </summary> static void consoleProcess_OnExited(Object sender, EventArgs ea) { consoleRunning = false; if( ConsoleExited != null ) ConsoleExited(null, EventArgs.Empty ); } [SecurityPermissionAttribute(SecurityAction.LinkDemand, Unrestricted=true)] protected override void WndProc(ref System.Windows.Forms.Message m) { if( m.Msg == 0x0002 )//WM_DESTROY Dispose(true);//idea here is to lose ownership of the console so console window will not be child of our window and not get destroyed. Dispose does just this base.WndProc(ref m); } protected override void Select(bool directed, bool forward) { base.Select(directed, forward); //as sideeffect also moves keybord focus to console. Win32.SetParent( ConsoleWindowHandle, clippingPanel.Handle ); } } }