Search code examples
vb.netfocusmodal-dialogtopmostcreateparams

Form on top but not clickable when modal dialog is shown


What I want is a small notification message that is shown in the lower right corner when there are any messages to be shown. If there are none the notification message will not be shown. The notification message should not steal focus or block the main application.

What I have is an application that runs a Task as a kind of messageservice. This application contains multiple dialogs that opens as modal dialogs.

When a message arrives to the application it is added to a observable list. This fires an eventhandler in the form showing the notification message and it is redrawn to show the first item in the list. When a message is read/closed it is removed from the list which fires the event again and the form is updated with the information from the first item in the list. If the list is empty the form is hidden.

My problem is that if i get a message and the notification message form is shown, and before I close it a modal dialog is opened in the main application, my form with the notification message is still on top of everything, even the modal dialog, but it's not clickable.

I've searched and read several forums for an answer but haven't been able to come up with an answer.

A small testapplication that simulates this behaviour can be found at Github. https://github.com/Oneleg/NotificationMessage

Some fast info:

The NotificationMessage form has:

  • FormBorderStyle = None
  • Topmost = False
  • Is shown with Show()
  • Overloads ShowWithoutActivation()
  • Overloads CreateParams with WS_EX_NOACTIVATE WS_EX_TOOLWINDOW WS_EX_TOPMOST

Any ideas on how I could solve this?


Solution

  • Looks like I'll be able to answer my own question.

    The answer is to create the NotificationMessage as an application withs it's own messagepump.

    Application.Run(New NotificationMessage(_messageList))
    

    After some modifications my Main now looks like this:

    Imports System.Threading
    Imports System.Threading.Tasks
    
    Public Class frmMain
    
        Private _notificationMessage As NotificationMessage
        Private _task As Task
        Private _messageList As ObservableGenericList(Of String) = New ObservableGenericList(Of String)
        Private ReadOnly _cancelMessages As CancellationTokenSource = New CancellationTokenSource()
    
        Private Sub btnModal_Click(sender As System.Object, e As System.EventArgs) Handles btnModal.Click
            frmModal.ShowDialog()
        End Sub
    
        Private Sub frmMain_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
            AddHandler _messageList.Changed, AddressOf MessageListChanged
        End Sub
    
        Private Sub NotificationMessageLoop(mess As String)
            _notificationMessage = New NotificationMessage(_messageList)
            _messageList.Add(mess)
            Application.Run(_notificationMessage)
        End Sub
    
        Private Sub btnMessage_Click(sender As System.Object, e As System.EventArgs) Handles btnMessage.Click
    
            Dim newMessage = String.Format("Message no {0}", _messageList.Count + 1)
    
            If _task Is Nothing Then
                _task = Task.Factory.StartNew(Sub() NotificationMessageLoop(newMessage), _cancelMessages.Token)
            Else
                _messageList.Add(newMessage)
            End If
        End Sub
    
        Private Sub MessageListChanged()
            If Not _messageList.Any Then
                _cancelMessages.Cancel()
            End If
        End Sub
    End Class
    

    And the NotificationMessage looks like this:

    Imports System.Runtime.InteropServices
    
    Public Class NotificationMessage
        Public Sub New(messages As ObservableGenericList(Of String))
    
            InitializeComponent()
            _messages = messages
            AddHandler _messages.Changed, AddressOf ListChanged
    
        End Sub
    
        Private ReadOnly _messages As ObservableGenericList(Of String)
        Private Delegate Sub ListChangedDelegate()
    
        Private Sub ListChanged()
            If InvokeRequired Then
                BeginInvoke(New ListChangedDelegate(AddressOf ListChanged))
                Return
            End If
    
            If _messages.Any Then
                Dim message As String = _messages.First
                txtMessage.Text = message
                lblCounter.Text = String.Format("({0} messages)", _messages.Count)
                Show()
            Else
                Hide()
            End If
        End Sub
    
        Private Sub MessageLoad(sender As System.Object, e As EventArgs) Handles MyBase.Load
            Left = Screen.PrimaryScreen.WorkingArea.Width - Width
            Top = Screen.PrimaryScreen.WorkingArea.Height - Height
        End Sub
    
        Private Sub btnClose_Click(sender As System.Object, e As System.EventArgs) Handles btnClose.Click
            _messages.RemoveFirst()
        End Sub
    
    #Region "Overrides"
    
        Private Const WS_EX_NOACTIVATE = &H8000000 ' Do not steal focus
        Private Const WS_EX_TOOLWINDOW = &H80 ' Makes form hidden from Alt + Tab window
        Private Const WS_EX_TOPMOST = &H8 ' Makes window topmost
    
        ''' <summary> Indicates whether the window will be activated when it is shown. </summary>
        ''' <remarks> http://msdn.microsoft.com/en-us/library/system.windows.forms.form.showwithoutactivation.aspx </remarks>
        Protected Overrides ReadOnly Property ShowWithoutActivation() As Boolean
            Get
                Return True
            End Get
        End Property
    
        ''' <summary> Override for creation parameters that are set when control handle is created. </summary>
        Protected Overrides ReadOnly Property CreateParams() As CreateParams
            Get
                Dim params As CreateParams = MyBase.CreateParams
                params.ExStyle = params.ExStyle Or WS_EX_NOACTIVATE Or WS_EX_TOOLWINDOW Or WS_EX_TOPMOST
                Return params
            End Get
        End Property
    
    #End Region
    
    End Class
    

    I now have a notification message that is only visible when there are any messages to show, doesn't steal focus when a new message arrives, is always on top and is clickable even after a modal form is opened in the main application.