Click here to Skip to main content
Click here to Skip to main content

MessageBox with a timeout for .NET

, 12 Aug 2004
Rate this:
Please Sign up or sign in to vote.
Just like MessageBox.Show but with an added timeout parameter

Introduction

Have you ever wanted a message box that times out? Well here you go.

Using the code

  1. Add MessageBoxEx.cs to your project or assembly your project references.
  2. Change MessageBox.Show(...); calls to MessageBoxEx.Show(..., timeout);.
  3. That's it!

Notes

  1. MessageBoxEx has all 12 of the MessageBox.Show overloads.
  2. The timeout is expressed in milliseconds.
  3. MessageBoxEx does not support reentrancy. Good thing too. Why would you want to have more than one message box at the same time?
  4. If the user doesn't press a button before the timeout elapses the function will return the DialogResult of the messagebox dialogs default button.

How does it work?

Just before calling MessageBox.Show(...), SetWindowsHookEx(WH_CALLWNDPROCRET, ...) is called. The hook proc looks for a WM_INITDIALOG on a window with text equal to the message box caption. A windows timer is started on that window with the appropriate timeout. When the timeout fires EndDialog is called with the result set to the dialog default button ID. You can get that ID by sending a dialog box a DM_GETDEFID message. Pretty simple.

References

KB318804: HOW TO: Set a Windows Hook in Visual C#.NET

Acknowledgements

GipsySoft: http://www.gipsysoft.com/messagebox. You'll find a C++ implementation with many more features.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

RodgerB
Software Developer (Senior) Bloomberg
United States United States
No Biography provided

Comments and Discussions

 
QuestionConverting to VB PinmemberRob Peterson4-Mar-14 2:19 
QuestionSystem.AppDomain.GetCurrentThreadId()' is obsolete PinmemberMember 1034965229-Oct-13 5:10 
QuestionGracias CApo! Pinmemberovamendocino23-Feb-13 5:37 
QuestionThanks Pinmembervasanthkumarmk17-Aug-12 2:40 
Questionit does not work when we use servicenotification message box (please suggest something) Pinmemberhsubhashis4-Jun-12 23:20 
AnswerRe: it does not work when we use servicenotification message box (please suggest something) PinmemberRodgerB4-Jun-12 23:34 
GeneralWow PinmemberWabiSabi16-Oct-11 4:52 
GeneralConverted to C++/CLI PinmemberSharjith1-Sep-11 9:07 
GeneralRe: Converted to C++/CLI PinmemberRodgerB1-Sep-11 9:28 
GeneralThank you! PinmemberChrisKane15-May-11 22:19 
GeneralMy vote of 5 Pinmembermatrixology4-May-11 5:06 
GeneralIts not working with DefaultDesktopOnly/ ServiceNotification option Pinmemberkirangiri4-Jan-11 7:26 
GeneralIts not working with DefaultDesktopOnly/ ServiceNotification option Pinmemberkirangiri4-Jan-11 7:26 
GeneralMy vote of 5 PinmemberFrancesco Pasquesi1-Dec-10 3:10 
GeneralSome enhancements PinmemberDanielLey25-Sep-10 6:59 
Thanks Rodger for the great posting; it helped me a lot!
 
Attached the vb.net code with a few enhancements:
 
1. The remaining time will be shown either in text or in caption if the placeholder {0} can be found.
2. The timeout can be set to 0 to have no timing.
3. Possibility to show a checkbox below the buttons - e.g.: Dont ask me again. The checkstate will be
returned as CheckAndExDialogResult
4. New DialogResults (ExDialogResult): Save, Discard, TryAgain and Continue to use this MessageBox also as
"Unsaved changes" dialog with save, discard and cancel results; therefore is does not longer return the
System.Windows.Forms.DialogResult.
5. Adding of language support: The button text will be replaced by GUI language - if there are native
speakers of below languages pls. feel free to correct the google translations. Thanks for that!!
Additional languages can be easy included: Just a the related enumeration and change the small part
within the sub: _setLanguage.
The languages are: English, German, French, Spain, Portugese, Polish, Czech and Dutch.
 
Here the code:
#Region " Imports "
Imports System.Text
Imports System.Runtime.InteropServices
Imports System.Security.Permissions
Imports System.Reflection
#End Region
 
'--------------------------------------------------------------------------
'Purpose:       Standard MessageBox with TimeOut
'Original code: http://www.codeproject.com/KB/miscctrl/CsMsgBoxTimeOut.aspx
'               Author: RodgerB / 13.08.2004
'               Licence: CPOL > http://www.codeproject.com/info/cpol10.aspx
'--------------------------------------------------------------------------
'Changed by:    Daniel Leykauf (23.09.2010)
'Changes:       - TimeOut can be set to 0 to have no automated closing
'               - If a placeholder ({0}) is used in Text or Caption, the 
'                 remaining time will be shown
'               - Possibility to show a CheckBox and to retrieve its value
'                 (e.g. "Don't remind me again.")
'               - Added of language support: Buttons will be shown in GUI 
'                 language instead of system language
'--------------------------------------------------------------------------

<Assembly: SecurityPermission(SecurityAction.RequestMinimum, UnmanagedCode:=True)> 
Namespace ExDialogs
 
    Public Enum ExDialogResult
        Abort = 3
        Cancel = 2
        Ignore = 5
        No = 7
        OK = 1
        Retry = 4
        Yes = 6
        TryAgain = 10
        [Continue] = 11
        Save = 12
        Discard = 13
    End Enum
 
    Public Enum ExMessageBoxButtons
        OK = 0
        OKCancel = 1
        AbortRetryIgnore = 2
        YesNoCancel = 3
        YesNo = 4
        RetryCancel = 5
        CancelTryContinue = 6
        SaveDiscardCancel = 7
    End Enum
 
    Public Class ExMessageBox
 
#Region " Show with checkbox "
        Public Shared Function Show(ByVal Text As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal uTimeout As UInteger) As CheckAndExDialogResult
            _setup([Assembly].GetExecutingAssembly.GetName.Name, uTimeout)
            mChecked = Checked
            mButtons = ExMessageBoxButtons.OK
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(Text, [Assembly].GetExecutingAssembly.GetName.Name)), Math.Abs(CInt(mChecked)))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal uTimeout As UInteger) As CheckAndExDialogResult
            _setup(caption, uTimeout)
            mButtons = ExMessageBoxButtons.OK
            mChecked = Checked
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(text, caption)), Math.Abs(CInt(mChecked)))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal buttons As ExMessageBoxButtons, ByVal uTimeout As UInteger) As CheckAndExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            mChecked = Checked
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(text, caption, _getButtons)), Math.Abs(CInt(mChecked)))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal uTimeout As UInteger) As CheckAndExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            mChecked = Checked
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(text, caption, _getButtons, icon)), Math.Abs(CInt(mChecked)))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal uTimeout As UInteger) As CheckAndExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            mChecked = Checked
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton)), Math.Abs(CInt(mChecked)))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal uTimeout As UInteger, ByVal displayHelpButton As Boolean) As CheckAndExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            mChecked = Checked
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton, 0, displayHelpButton)), Math.Abs(CInt(mChecked)))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal options As MessageBoxOptions, _
          ByVal uTimeout As UInteger) As CheckAndExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            mChecked = Checked
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton, options)), Math.Abs(CInt(mChecked)))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal options As MessageBoxOptions, _
           ByVal uTimeout As UInteger, ByVal displayHelpButton As Boolean) As CheckAndExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            mChecked = Checked
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton, options, displayHelpButton)), Math.Abs(CInt(mChecked)))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal CheckMsgText As String, ByVal Checked As Boolean, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal options As MessageBoxOptions, _
           ByVal uTimeout As UInteger, ByVal HelpFile As String) As CheckAndExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            mChecked = Checked
            mCheckMsgText = CheckMsgText
            Return New CheckAndExDialogResult(_getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton, options, HelpFile)), Math.Abs(CInt(mChecked)))
        End Function
 
#End Region
 
#Region " Show default // "
        Public Shared Function Show(ByVal text As String, ByVal uTimeout As UInteger) As ExDialogResult
            _setup([Assembly].GetExecutingAssembly.GetName.Name, uTimeout)
            mButtons = ExMessageBoxButtons.OK
            Return _getResult(MessageBox.Show(text, [Assembly].GetExecutingAssembly.GetName.Name))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = ExMessageBoxButtons.OK
            Return _getResult(MessageBox.Show(text, caption))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return _getResult(MessageBox.Show(text, caption, _getButtons))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return _getResult(MessageBox.Show(text, caption, _getButtons, icon))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return _getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal uTimeout As UInteger, ByVal displayHelpButton As Boolean) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return _getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton, 0, displayHelpButton))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal options As MessageBoxOptions, _
          ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return _getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton, options))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal options As MessageBoxOptions, _
           ByVal uTimeout As UInteger, ByVal displayHelpButton As Boolean) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return _getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton, options, displayHelpButton))
        End Function
 
        Public Shared Function Show(ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, ByVal options As MessageBoxOptions, _
           ByVal uTimeout As UInteger, ByVal HelpFile As String) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return _getResult(MessageBox.Show(text, caption, _getButtons, icon, defButton, options, HelpFile))
        End Function
 
#End Region
 
#Region " Show with owner "
        Public Shared Function Show(ByVal owner As IWin32Window, ByVal text As String, ByVal uTimeout As UInteger) As ExDialogResult
            _setup([Assembly].GetExecutingAssembly.GetName.Name, uTimeout)
            mButtons = ExMessageBoxButtons.OK
            Return MessageBox.Show(owner, text, [Assembly].GetExecutingAssembly.GetName.Name)
        End Function
 
        Public Shared Function Show(ByVal owner As IWin32Window, ByVal text As String, ByVal caption As String, ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = ExMessageBoxButtons.OK
            Return MessageBox.Show(owner, text, caption)
        End Function
 
        Public Shared Function Show(ByVal owner As IWin32Window, ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return MessageBox.Show(owner, text, caption, _getButtons)
        End Function
 
        Public Shared Function Show(ByVal owner As IWin32Window, ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return MessageBox.Show(owner, text, caption, _getButtons, icon)
        End Function
 
        Public Shared Function Show(ByVal owner As IWin32Window, ByVal text As String, ByVal caption As String, ByVal buttons As ExMessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, _
         Optional ByVal uTimeout As UInteger = 0) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return MessageBox.Show(owner, text, caption, _getButtons, icon, defButton)
        End Function
 
        Public Shared Function Show(ByVal owner As IWin32Window, ByVal text As String, ByVal caption As String, ByVal buttons As MessageBoxButtons, ByVal icon As MessageBoxIcon, ByVal defButton As MessageBoxDefaultButton, _
         ByVal options As MessageBoxOptions, ByVal uTimeout As UInteger) As ExDialogResult
            _setup(caption, uTimeout)
            mButtons = buttons
            Return MessageBox.Show(owner, text, caption, _getButtons, icon, defButton, options)
        End Function
#End Region
 
#Region "Save Dialog"
        Public Shared Function Save(ByVal text As String) As ExDialogResult
            _setup([Assembly].GetExecutingAssembly.GetName.Name, 0)
            mButtons = ExMessageBoxButtons.SaveDiscardCancel
            Return _getResult(MessageBox.Show(text, [Assembly].GetExecutingAssembly.GetName.Name, _getButtons, MessageBoxIcon.Exclamation))
        End Function
 
        Public Shared Function Save(ByVal text As String, ByVal defaultButton As MessageBoxDefaultButton) As ExDialogResult
            _setup([Assembly].GetExecutingAssembly.GetName.Name, 0)
            mButtons = ExMessageBoxButtons.SaveDiscardCancel
            Return _getResult(MessageBox.Show(text, [Assembly].GetExecutingAssembly.GetName.Name, _getButtons, MessageBoxIcon.Exclamation, defaultButton))
        End Function
 
        Public Shared Function Save(ByVal text As String, ByVal caption As String) As DialogResult
            _setup(caption, 0)
            mButtons = ExMessageBoxButtons.SaveDiscardCancel
            Return _getResult(MessageBox.Show(text, caption, _getButtons, MessageBoxIcon.Exclamation))
        End Function
 
        Public Shared Function Save(ByVal text As String, ByVal caption As String, ByVal defaultButton As MessageBoxDefaultButton) As ExDialogResult
            _setup(caption, 0)
            mButtons = ExMessageBoxButtons.SaveDiscardCancel
            Return _getResult(MessageBox.Show(text, caption, _getButtons, MessageBoxIcon.Exclamation, defaultButton))
        End Function
 
#End Region
 

 
#Region " Delegates "
        Public Delegate Function HookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
        Public Delegate Sub TimerProc(ByVal hWnd As IntPtr, ByVal uMsg As UInteger, ByVal nIDEvent As UIntPtr, ByVal dwTime As UInteger)
#End Region
 
#Region " Enums "
        <Flags()> _
    Private Enum SetWindowPosFlags As UInteger
            ''' <summary>If the calling thread and the thread that owns the window are attached to different input queues, 
            ''' the system posts the request to the thread that owns the window. This prevents the calling thread from 
            ''' blocking its execution while other threads process the request.</summary>
            ''' <remarks>SWP_ASYNCWINDOWPOS</remarks>
            SynchronousWindowPosition = &H4000
            ''' <summary>Prevents generation of the WM_SYNCPAINT message.</summary>
            ''' <remarks>SWP_DEFERERASE</remarks>
            DeferErase = &H2000
            ''' <summary>Draws a frame (defined in the window's class description) around the window.</summary>
            ''' <remarks>SWP_DRAWFRAME</remarks>
            DrawFrame = &H20
            ''' <summary>Applies new frame styles set using the SetWindowLong function. Sends a WM_NCCALCSIZE message to 
            ''' the window, even if the window's size is not being changed. If this flag is not specified, WM_NCCALCSIZE 
            ''' is sent only when the window's size is being changed.</summary>
            ''' <remarks>SWP_FRAMECHANGED</remarks>
            FrameChanged = &H20
            ''' <summary>Hides the window.</summary>
            ''' <remarks>SWP_HIDEWINDOW</remarks>
            HideWindow = &H80
            ''' <summary>Does not activate the window. If this flag is not set, the window is activated and moved to the 
            ''' top of either the topmost or non-topmost group (depending on the setting of the hWndInsertAfter 
            ''' parameter).</summary>
            ''' <remarks>SWP_NOACTIVATE</remarks>
            DoNotActivate = &H10
            ''' <summary>Discards the entire contents of the client area. If this flag is not specified, the valid 
            ''' contents of the client area are saved and copied back into the client area after the window is sized or 
            ''' repositioned.</summary>
            ''' <remarks>SWP_NOCOPYBITS</remarks>
            DoNotCopyBits = &H100
            ''' <summary>Retains the current position (ignores X and Y parameters).</summary>
            ''' <remarks>SWP_NOMOVE</remarks>
            IgnoreMove = &H2
            ''' <summary>Does not change the owner window's position in the Z order.</summary>
            ''' <remarks>SWP_NOOWNERZORDER</remarks>
            DoNotChangeOwnerZOrder = &H200
            ''' <summary>Does not redraw changes. If this flag is set, no repainting of any kind occurs. This applies to 
            ''' the client area, the nonclient area (including the title bar and scroll bars), and any part of the parent 
            ''' window uncovered as a result of the window being moved. When this flag is set, the application must 
            ''' explicitly invalidate or redraw any parts of the window and parent window that need redrawing.</summary>
            ''' <remarks>SWP_NOREDRAW</remarks>
            DoNotRedraw = &H8
            ''' <summary>Same as the SWP_NOOWNERZORDER flag.</summary>
            ''' <remarks>SWP_NOREPOSITION</remarks>
            DoNotReposition = &H200
            ''' <summary>Prevents the window from receiving the WM_WINDOWPOSCHANGING message.</summary>
            ''' <remarks>SWP_NOSENDCHANGING</remarks>
            DoNotSendChangingEvent = &H400
            ''' <summary>Retains the current size (ignores the cx and cy parameters).</summary>
            ''' <remarks>SWP_NOSIZE</remarks>
            IgnoreResize = &H1
            ''' <summary>Retains the current Z order (ignores the hWndInsertAfter parameter).</summary>
            ''' <remarks>SWP_NOZORDER</remarks>
            IgnoreZOrder = &H4
            ''' <summary>Displays the window.</summary>
            ''' <remarks>SWP_SHOWWINDOW</remarks>
            ShowWindow = &H40
        End Enum
 
        Private Enum DialogStyles
            [Default]
            Save
        End Enum
 

#End Region
 
#Region " User32.dll "
        <DllImport("User32.dll")> _
        Public Shared Function SetTimer(ByVal hWnd As IntPtr, ByVal nIDEvent As UIntPtr, ByVal uElapse As UInteger, ByVal lpTimerFunc As TimerProc) As UIntPtr
        End Function
 
        <DllImport("User32.dll")> _
        Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal Msg As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
        End Function
 
        <DllImport("user32.dll")> _
        Public Shared Function SetWindowsHookEx(ByVal idHook As Integer, ByVal lpfn As HookProc, ByVal hInstance As IntPtr, ByVal threadId As Integer) As IntPtr
        End Function
 
        <DllImport("user32.dll")> _
        Public Shared Function UnhookWindowsHookEx(ByVal idHook As IntPtr) As Integer
        End Function
 
        <DllImport("user32.dll")> _
        Public Shared Function CallNextHookEx(ByVal idHook As IntPtr, ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
        End Function
 
        <DllImport("user32.dll")> _
        Public Shared Function GetWindowTextLength(ByVal hWnd As IntPtr) As Integer
        End Function
 
        <DllImport("user32.dll")> _
        Public Shared Function GetWindowText(ByVal hWnd As IntPtr, ByVal text As StringBuilder, ByVal maxLength As Integer) As Integer
        End Function
 
        <DllImport("user32.dll")> _
        Public Shared Function EndDialog(ByVal hDlg As IntPtr, ByVal nResult As IntPtr) As Integer
        End Function
 
        <DllImport("kernel32.dll")> _
        Private Shared Function GetCurrentThreadId() As Integer
        End Function
 
        <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
        Private Shared Function SetWindowText(ByVal hwnd As IntPtr, ByVal lpString As String) As Boolean
        End Function
 
        <DllImport("user32.dll")> _
         Private Shared Function SetDlgItemText(ByVal hWnd As IntPtr, ByVal nIDDlgItem As Integer, ByVal lpString As String) As Boolean
        End Function
 
        <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
        Private Shared Function GetDlgItemText(ByVal hDlg As IntPtr, ByVal nIDDlgItem As Integer, <Out()> ByVal lpString As StringBuilder, ByVal nMaxCount As Integer) As UInteger
        End Function
 
        <DllImport("user32.dll", SetLastError:=True)> _
        Private Shared Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As IntPtr, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal uFlags As SetWindowPosFlags) As Boolean
        End Function
 
        <DllImport("user32.dll")> _
        Private Shared Function GetWindowRect(ByVal hWnd As IntPtr, ByRef lpRect As RECT) As Boolean
        End Function
 
        <DllImport("user32.dll", CharSet:=CharSet.Auto)> _
        Private Shared Function CreateWindowEx(ByVal dwExStyle As Integer, ByVal lpClassName As String, ByVal lpWindowName As String, ByVal dwStyle As UInteger, ByVal x As Integer, ByVal y As Integer, ByVal nWidth As Integer, ByVal nHeight As Integer, ByVal hWndParent As Integer, ByVal hMenu As Integer, ByVal hInstance As Integer, ByVal lpParam As Integer) As Integer
        End Function
 
        <DllImport("user32.dll")> _
        Private Shared Function DestroyWindow(ByVal hwnd As Integer) As Boolean
        End Function
 
#End Region
 
#Region " Structures "
        <StructLayout(LayoutKind.Sequential)> _
        Public Structure CWPRETSTRUCT
            Public lResult As IntPtr
            Public lParam As IntPtr
            Public wParam As IntPtr
            Public message As UInteger
            Public hwnd As IntPtr
        End Structure
 
        <StructLayout(LayoutKind.Sequential)> _
    Public Structure RECT
            Private _Left As Integer, _Top As Integer, _Right As Integer, _Bottom As Integer
 
            Public Sub New(ByVal Rectangle As Rectangle)
                Me.New(Rectangle.Left, Rectangle.Top, Rectangle.Right, Rectangle.Bottom)
            End Sub
            Public Sub New(ByVal Left As Integer, ByVal Top As Integer, ByVal Right As Integer, ByVal Bottom As Integer)
                _Left = Left
                _Top = Top
                _Right = Right
                _Bottom = Bottom
            End Sub
 
            Public Property X() As Integer
                Get
                    Return _Left
                End Get
                Set(ByVal value As Integer)
                    _Left = value
                End Set
            End Property
            Public Property Y() As Integer
                Get
                    Return _Top
                End Get
                Set(ByVal value As Integer)
                    _Top = value
                End Set
            End Property
            Public Property Left() As Integer
                Get
                    Return _Left
                End Get
                Set(ByVal value As Integer)
                    _Left = value
                End Set
            End Property
            Public Property Top() As Integer
                Get
                    Return _Top
                End Get
                Set(ByVal value As Integer)
                    _Top = value
                End Set
            End Property
            Public Property Right() As Integer
                Get
                    Return _Right
                End Get
                Set(ByVal value As Integer)
                    _Right = value
                End Set
            End Property
            Public Property Bottom() As Integer
                Get
                    Return _Bottom
                End Get
                Set(ByVal value As Integer)
                    _Bottom = value
                End Set
            End Property
            Public Property Height() As Integer
                Get
                    Return _Bottom - _Top
                End Get
                Set(ByVal value As Integer)
                    _Bottom = value - _Top
                End Set
            End Property
            Public Property Width() As Integer
                Get
                    Return _Right - _Left
                End Get
                Set(ByVal value As Integer)
                    _Right = value + _Left
                End Set
            End Property
            Public Property Location() As Point
                Get
                    Return New Point(Left, Top)
                End Get
                Set(ByVal value As Point)
                    _Left = value.X
                    _Top = value.Y
                End Set
            End Property
            Public Property Size() As Size
                Get
                    Return New Size(Width, Height)
                End Get
                Set(ByVal value As Size)
                    _Right = value.Width + _Left
                    _Bottom = value.Height + _Top
                End Set
            End Property
 
            Public Shared Widening Operator CType(ByVal Rectangle As RECT) As Rectangle
                Return New Rectangle(Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height)
            End Operator
            Public Shared Widening Operator CType(ByVal Rectangle As Rectangle) As RECT
                Return New RECT(Rectangle.Left, Rectangle.Top, Rectangle.Right, Rectangle.Bottom)
            End Operator
            Public Shared Operator =(ByVal Rectangle1 As RECT, ByVal Rectangle2 As RECT) As Boolean
                Return Rectangle1.Equals(Rectangle2)
            End Operator
            Public Shared Operator <>(ByVal Rectangle1 As RECT, ByVal Rectangle2 As RECT) As Boolean
                Return Not Rectangle1.Equals(Rectangle2)
            End Operator
 
            Public Overrides Function ToString() As String
                Return "{Left: " & _Left & "; " & "Top: " & _Top & "; Right: " & _Right & "; Bottom: " & _Bottom & "}"
            End Function
 
            Public Overloads Function Equals(ByVal Rectangle As RECT) As Boolean
                Return Rectangle.Left = _Left AndAlso Rectangle.Top = _Top AndAlso Rectangle.Right = _Right AndAlso Rectangle.Bottom = _Bottom
            End Function
            Public Overloads Overrides Function Equals(ByVal [Object] As Object) As Boolean
                If TypeOf [Object] Is RECT Then
                    Return Equals(DirectCast([Object], RECT))
                ElseIf TypeOf [Object] Is Rectangle Then
                    Return Equals(New RECT(DirectCast([Object], Rectangle)))
                End If
 
                Return False
            End Function
        End Structure
#End Region
 
#Region " Consts "
        Private Const TimerID As Integer = 42
        Private Const WH_CALLWNDPROCRET As Integer = 12
        Private Const WM_DESTROY As Integer = &H2
        Private Const WM_INITDIALOG As Integer = &H110
        Private Const WM_TIMER As Integer = &H113
        Private Const WM_USER As Integer = &H400
        Private Const DM_GETDEFID As Integer = WM_USER + 0
        Private Const WM_COMMAND As UInteger = &H111
        Private Const WS_VISIBLE As UInteger = &H10000000
        Private Const WS_CHILD As UInteger = &H40000000
        Private Const WS_TABSTOP As UInteger = &H10000
        Private Const WM_SETFONT As Integer = &H30
        Private Const WM_GETFONT As Integer = &H31
        Private Const BS_AUTOCHECKBOX As UInteger = &H3
        Private Const WM_NOTIFY As Integer = &H4E
        Private Const CB_SETCURSEL As Integer = &H14E
        Private Const CB_GETCURSEL As Integer = &H147
        Private Const BM_GETCHECK As Integer = &HF0
        Private Const BM_SETCHECK As Integer = &HF1
        Private Const BST_CHECKED As Integer = &H1
        Private Const BST_INDETERMINATE As Integer = &H2
        Private Const BST_UNCHECKED As Integer = &H0
#End Region
 
#Region " Variables "
        Private Shared mHookProc As HookProc
        Private Shared mHookTimer As TimerProc
        Private Shared mHookTimeout As Integer
        Private Shared mHookCaption As String
        Private Shared mHook As IntPtr
        Private Shared mHandle As Long
        Private Shared WithEvents mTimer As New Timer
        Private Shared mCaption As String = String.Empty
        Private Shared mdteStart As Date
        Private Shared mCheckBoxHandle As IntPtr
        Private Shared mCheckMsgText As String = String.Empty
        Private Shared mButtons As ExMessageBoxButtons
        Private Shared ReadOnly HWND_BOTTOM As New IntPtr(1)
        Private Shared ReadOnly HWND_NOTOPMOST As New IntPtr(-2)
        Private Shared ReadOnly HWND_TOP As New IntPtr(0)
        Private Shared ReadOnly HWND_TOPMOST As New IntPtr(-1)
        Private Shared mChecked As Boolean
        Private Shared mText As String = ""
#End Region
 
#Region " Public methods "
        Shared Sub New()
            mHookProc = New HookProc(AddressOf _messageBoxHookProc)
            mHookTimer = New TimerProc(AddressOf _messageBoxTimerProc)
            mHookTimeout = 0
            mHookCaption = Nothing
            mHook = IntPtr.Zero
        End Sub
#End Region
 
#Region " Private methods "
        Private Shared Function _getResult(ByVal r As DialogResult) As ExDialogResult
            Select Case mButtons
                Case ExMessageBoxButtons.SaveDiscardCancel
                    Select Case r
                        Case DialogResult.Yes
                            Return ExDialogResult.Save
                        Case DialogResult.No
                            Return ExDialogResult.Discard
                        Case Else
                            Return ExDialogResult.Cancel
                    End Select
                Case ExMessageBoxButtons.CancelTryContinue
                    Select Case r
                        Case DialogResult.Yes
                            Return ExDialogResult.Cancel
                        Case DialogResult.No
                            Return ExDialogResult.TryAgain
                        Case Else
                            Return ExDialogResult.Continue
                    End Select
                Case Else
                    Return CType(CInt(r), ExDialogResult)
            End Select
        End Function
 
        Private Shared Function _getButtons() As MessageBoxButtons
            Select Case mButtons
                Case ExMessageBoxButtons.AbortRetryIgnore
                    Return MessageBoxButtons.AbortRetryIgnore
                Case ExMessageBoxButtons.CancelTryContinue
                    Return MessageBoxButtons.YesNoCancel
                Case ExMessageBoxButtons.OK
                    Return MessageBoxButtons.OK
                Case ExMessageBoxButtons.OKCancel
                    Return MessageBoxButtons.OKCancel
                Case ExMessageBoxButtons.RetryCancel
                    Return MessageBoxButtons.RetryCancel
                Case ExMessageBoxButtons.SaveDiscardCancel
                    Return MessageBoxButtons.YesNoCancel
                Case ExMessageBoxButtons.YesNo
                    Return MessageBoxButtons.YesNo
                Case ExMessageBoxButtons.YesNoCancel
                    Return MessageBoxButtons.YesNoCancel
            End Select
        End Function
 
        Private Shared Sub _setup(ByVal caption As String, ByVal uTimeout As UInteger)
            If mHook <> IntPtr.Zero Then
                Throw New NotSupportedException("multiple calls are not supported")
            End If
 
            mCaption = caption
            If uTimeout > 0 Then mHookTimeout = uTimeout
            mHookCaption = If(caption IsNot Nothing, caption, "")
            mdteStart = Now
 
            mHook = SetWindowsHookEx(WH_CALLWNDPROCRET, mHookProc, IntPtr.Zero, GetCurrentThreadId())
            If mHookTimeout > 0 Then
                mTimer.Interval = 10
                mTimer.Enabled = True
            End If
        End Sub
 
        Private Shared Function _messageBoxHookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
            If nCode < 0 Then
                Return CallNextHookEx(mHook, nCode, wParam, lParam)
            End If
 
            Dim msg As CWPRETSTRUCT = CType(Marshal.PtrToStructure(lParam, GetType(CWPRETSTRUCT)), CWPRETSTRUCT)
            Dim hook As IntPtr = mHook
 
            mHandle = msg.hwnd
 

            If mHookCaption IsNot Nothing AndAlso msg.message = WM_INITDIALOG Then
                Dim nLength As Integer = GetWindowTextLength(msg.hwnd)
                Dim text As New StringBuilder(nLength + 1)
 
                GetWindowText(msg.hwnd, text, text.Capacity)
 
                If mHookCaption = text.ToString() AndAlso mHookTimeout > 0 Then
                    mHookCaption = Nothing
                    SetTimer(msg.hwnd, CType(TimerID, UIntPtr), mHookTimeout, mHookTimer)
                    UnhookWindowsHookEx(mHook)
                    mHook = IntPtr.Zero
                End If
 
                'set button language
                _setLanguage(msg.hwnd)
 

 
                If mCheckMsgText.Length > 0 Then
                    Dim r As RECT
                    GetWindowRect(msg.hwnd, r)
                    SetWindowPos(msg.hwnd, HWND_TOP, r.X, r.Y, r.Width, r.Height + 30, SetWindowPosFlags.FrameChanged)
                    Dim intWidth As Integer = TextRenderer.MeasureText(mCheckMsgText, SystemFonts.DialogFont).Width + 10
 
                    'create checkbox
                    Dim chkHandle As Integer = CreateWindowEx(0, "BUTTON", "myCheckBox", BS_AUTOCHECKBOX Or WS_VISIBLE Or WS_CHILD Or WS_TABSTOP, 10, r.Height - 30, _
                      intWidth, 20, msg.hwnd, 0, 0, 0)
                    SetWindowText(chkHandle, mCheckMsgText)
                    Dim fontHandle As Integer = SendMessage(msg.hwnd, WM_GETFONT, 0, 0)
                    SendMessage(chkHandle, WM_SETFONT, fontHandle, 0)
                    mCheckBoxHandle = chkHandle
 
                    'set checkstate
                    If mChecked Then
                        SendMessage(mCheckBoxHandle, BM_SETCHECK, BST_CHECKED, 0&)
                    Else
                        SendMessage(mCheckBoxHandle, BM_SETCHECK, BST_UNCHECKED, 0&)
                    End If
                End If
 
            ElseIf msg.message = WM_DESTROY Then
                If mCheckBoxHandle <> 0 Then DestroyWindow(mCheckBoxHandle)
            ElseIf msg.message = WM_COMMAND Then
                'get checkstate
                mChecked = SendMessage(mCheckBoxHandle, BM_GETCHECK, 0&, 0&)
            End If
 
            Return CallNextHookEx(hook, nCode, wParam, lParam)
        End Function
 
        Private Shared Sub _messageBoxTimerProc(ByVal hWnd As IntPtr, ByVal uMsg As UInteger, ByVal nIDEvent As UIntPtr, ByVal dwTime As UInteger)
            If nIDEvent = CType(TimerID, UIntPtr) Then
                mTimer.Enabled = False
                mTimer.Dispose()
                'get checkstate
                mChecked = SendMessage(mCheckBoxHandle, BM_GETCHECK, 0&, 0&)
                EndDialog(hWnd, CType(_getLowWord(SendMessage(hWnd, DM_GETDEFID, IntPtr.Zero, IntPtr.Zero)), IntPtr))
            End If
        End Sub
 
        Private Shared mTextTemp As String = String.Empty
        Private Shared Sub _timer_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles mTimer.Tick
            If mTimer.Interval = 10 Then mTimer.Interval = 250
 
            Dim intSec As Integer = Math.Round(mHookTimeout / 1000 - CType(Now - mdteStart, TimeSpan).TotalSeconds, 0)
            Dim str As String = String.Format(mCaption, intSec)
            If mCaption.Contains("{0}") Then SetWindowText(mHandle, str)
 
            Static intStart As Integer = 0
 
            If mHandle <> IntPtr.Zero Then
                If intStart = 0 Then
                    'get original text 
                    Dim sb As New StringBuilder(256)
                    GetDlgItemText(mHandle, &HFFFF, sb, sb.MaxCapacity)
                    If sb.ToString.Length > 0 Then mText = sb.ToString
                    intStart += 1
                End If
 
                If mText.Contains("{0}") Then
                    If Not mTextTemp = String.Format(mText, intSec) Then
                        SetDlgItemText(mHandle, &HFFFF, String.Format(mText, intSec))
                    End If
                    mTextTemp = String.Format(mText, intSec)
                End If
            End If
 
            mChecked = SendMessage(mCheckBoxHandle, BM_GETCHECK, 0&, 0&)
 
        End Sub
 
        Private Shared Function _getLowWord(ByRef pintValue As Int32) As Int32
            Return pintValue And &HFFFF
        End Function
 
        Private Shared Function _getLowWord(ByRef pudtValue As IntPtr) As Int32
            Return _getLowWord(pudtValue.ToInt32)
        End Function
 
        Private Shared Function _getHighWord(ByRef pintValue As Int32) As Int32
            If (pintValue And &H80000000) = &H80000000 Then
                Return ((pintValue And &H7FFF0000) \ &H10000) Or &H8000&
            Else
                Return (pintValue And &HFFFF0000) \ &H10000
            End If
        End Function
 
        Private Shared Sub _setLanguage(ByVal hwd As IntPtr)
            Dim ty As Type = GetType(en)
            Select Case System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName.ToLower
                Case "de"
                    ty = GetType(de)
                Case "fr"
                    ty = GetType(fr)
                Case "pl"
                    ty = GetType(pl)
                Case "cs"
                    ty = GetType(cs)
                Case "es"
                    ty = GetType(es)
                Case "pt"
                    ty = GetType(pt)
                Case "nl"
                    ty = GetType(nl)
            End Select
 
            Select Case mButtons
                Case ExMessageBoxButtons.SaveDiscardCancel
                    SetDlgItemText(hwd, 6, "&" & Replace(Split([Enum].GetName(ty, 12), "__")(1), "_", " "))
                    SetDlgItemText(hwd, 7, "&" & Replace(Split([Enum].GetName(ty, 13), "__")(1), "_", " "))
                    SetDlgItemText(hwd, 2, "&" & Replace(Split([Enum].GetName(ty, 2), "__")(1), "_", " "))
                Case ExMessageBoxButtons.CancelTryContinue
                    SetDlgItemText(hwd, 6, "&" & Replace(Split([Enum].GetName(ty, 2), "__")(1), "_", " "))
                    SetDlgItemText(hwd, 7, "&" & Replace(Split([Enum].GetName(ty, 10), "__")(1), "_", " "))
                    SetDlgItemText(hwd, 2, "&" & Replace(Split([Enum].GetName(ty, 11), "__")(1), "_", " "))
                Case Else
                    For Each i As Integer In [Enum].GetValues(ty)
                        If Not mButtons = ExMessageBoxButtons.OK Then
                            SetDlgItemText(hwd, i, "&" & Replace(Split([Enum].GetName(ty, i), "__")(1), "_", " "))
                        Else
                            'if there is only the ok button then is dlgid 2 instead 1
                            SetDlgItemText(hwd, 2, "&" & Replace(Split([Enum].GetName(ty, 1), "__")(1), "_", " "))
                        End If
                    Next
            End Select
 

        End Sub
#End Region
 
#Region " Languages "
        Enum de
            Abort__Abbrechen = 3
            Cancel__Abbrechen = 2
            Ignore__Ignorieren = 5
            No__Nein = 7
            OK__OK = 1
            Retry_Wiederholen = 4
            Yes__Ja = 6
            Help__Hilfe = 9
            TryAgain__Wiederholen = 10
            Continue__Fortsetzen = 11
            Save__Speichern = 12
            Discard__Verwerfen = 13
        End Enum
 
        Enum en
            Abort__Abort = 3
            Cancel__Cancel = 2
            Ignore__Ignore = 5
            No__No = 7
            OK__OK = 1
            Retry__Retry = 4
            Yes__Yes = 6
            Help__Help = 9
            TryAgain__Try_Again = 10
            Continue__Continue = 11
            Save__Save = 12
            Discard__Discard = 13
        End Enum
 
        Enum fr
            Abort__Arrêt = 3
            Cancel__Annuler = 2
            Ignore__Ignorer = 5
            No__Non = 7
            OK__OK = 1
            Retry__Réessayer = 4
            Yes__Oui = 6
            Help__Aide = 9
            TryAgain__Réessayer = 10
            Continue__Continuer = 11
            Save__Sauver = 12
            Discard__Jeter = 13
        End Enum
 
        Enum cs
            Abort__Potratit = 3
            Cancel__Zrušit = 2
            Ignore__Ignorovat = 5
            No__Ne = 7
            OK__OK = 1
            Retry__Opakovat = 4
            Yes__Ano = 6
            Help__Pomoc = 9
            TryAgain__Zkusit = 10
            Continue__Pokračovat = 11
            Save__Uložit = 12
            Discard__Odhodit = 13
        End Enum
 
        Enum pl
            Abort__Przerwij = 3
            Cancel__Anulować = 2
            Ignore__Zignorować = 5
            No__Nie = 7
            OK__OK = 1
            Retry__Powtarzanie = 4
            Yes__Tak = 6
            Help__Pomoc = 9
            TryAgain__Próbować = 10
            Continue__Kontynuować = 11
            Save__Zaoszczędzić = 12
            Discard__Odrzuć = 13
        End Enum
 
        Enum es
            Abort__Abortar = 3
            Cancel__Cancelar = 2
            Ignore__Pasar = 5
            No__No = 7
            OK__Aceptar = 1
            Retry_Intentar = 4
            Yes__Sí = 6
            Help__Ayuda = 9
            TryAgain__Intentar = 10
            Continue__Continuar = 11
            Save__Guardar = 12
            Discard__Descartar = 13
        End Enum
 
        Enum pt
            Abort__Abortar = 3
            Cancel__Cancelar = 2
            Ignore__Ignorar = 5
            No__Não = 7
            OK__OK = 1
            Retry__Tentar = 4
            Yes__Sim = 6
            Help__Ajuda = 9
            TryAgain__Intentar = 10
            Continue__Continuar = 11
            Save__Guardar = 12
            Discard__Descartar = 13
        End Enum
 
        Enum nl
            Abort__Afbreken = 3
            Cancel__Annuleren = 2
            Ignore__Negeren = 5
            No__Nee = 7
            OK__OK = 1
            Retry__Herhaal = 4
            Yes__Ja = 6
            Help__Help = 9
            TryAgain__Proberen = 10
            Continue__Voortzetten = 11
            Save__Besparen = 12
            Discard__Ontdoen = 13
        End Enum
#End Region
 
#Region " CheckAndExDialogResult "
        Public Class CheckAndExDialogResult
            Private mChecked As CheckState
            Private mResult As ExDialogResult
 
            ReadOnly Property CheckState() As CheckState
                Get
                    Return mChecked
                End Get
            End Property
 
            ReadOnly Property ExDialogResult() As ExDialogResult
                Get
                    Return mResult
                End Get
            End Property
 
            Sub New(ByVal dResult As ExDialogResult, ByVal cResult As CheckState)
                mResult = dResult
                mChecked = cResult
            End Sub
        End Class
#End Region
    End Class
End Namespace

GeneralMy vote of 5 PinmemberTomChantler22-Sep-10 4:59 
GeneralThis isn't working in win7, Visual Studio 2008 PinmemberMr C P9-Mar-10 23:10 
GeneralRe: This isn't working in win7, Visual Studio 2008 PinmemberMr C P9-Mar-10 23:15 
GeneralRe: This isn't working in win7, Visual Studio 2008 PinmemberTomChantler22-Sep-10 5:00 
Generalgood article PinmemberDonsw17-Jan-09 17:51 
GeneralVery very very good code, but... Pinmemberseb.4924-Apr-08 5:20 
QuestionDoes this support multi threading calls PinmemberKanna22-Mar-08 5:23 
GeneralRe: Does this support multi threading calls PinmemberRodgerB31-Mar-08 13:00 
GeneralPocketPC Pinmemberrichardjjs14-Apr-07 21:56 
GeneralRe: PocketPC [modified] PinmemberRodgerB15-Apr-07 4:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140709.1 | Last Updated 13 Aug 2004
Article Copyright 2004 by RodgerB
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid