Introduction
I like to keep my users informed. I know I'm probably way out of line here, but hey, I'm a rebel.
We have a lot of systems we deal with, don't we? Email, servers, message queues, servlets, web services, etc. And more and more, we are integrating these into the applications we write. Some applications may require all of these and more. If a part is down, it may be inconvenient to the users. For instance, how about this scenario: as part of your application, a user receives messages from a message queue, manipulates the data with your application, then posts the resulting data to a remote web service, and at the same time, notifies the next department by email that will receive the data that it is complete.
We put exception handling into our apps to handle such situations, right? What about the user...it's frustrating for them if they go to submit something to the remote web service only to find the corporate web connection is down. Or if they try to send the notification email, only to find that the mail server is down for maintenance. It would be nice for them to know prior to this so they can be prepared. After all, we want our users to be happy, right? (I'm kinda sappy that way.)
The Control
In our environment, the systems gurus have graciously done this by creating a web page notifying the entire organization of any problems that are currently occurring, any scheduled outages, and any special circumstances they feel the need to communicate. The problem is, you have to open up a browser, navigate to the page, and refresh the page before getting the current status.
I've included in my application this OSNotify
control to display messages about system status or anything else I want the user to pay attention to. It works as follows:
The control contains an OSMessage
class that defines several properties; Key
, Index
, Text
, Importance
, Link
, and Ignore
. It also has a collection class, OSMessages
, that can be passed to the control to display. Messages can be marked at three importance levels, LOW
, MEDIUM
, HIGH
, and different user-defined icons will be displayed for each. The Ignore
property can be used to skip a message if the user doesn't want to see it anymore.
Many properties control the display of the messages; these are described below:
BorderStyle
, BackColor
, ForeColor
- you know what these are.
ChangeInterval
, ScrollSize
- in combination, sets scrolling speed.
LowPriorityIcon
, MediumPriorityIcon
, HighPriorityIcon
- self explanatory.
MessageDisplayType
- either osStatic
or osRightToLeft
. If osStatic
, messages will change at the ChangeInterval
event timeout; if RightToLeft
, message is scrolled to the left, the number of ScrollSize
(pixels) at the ChangeInterval
event timeout.
MessageIsLink
- if true will navigate to the document identified in the OSMessage.Link
property.
PauseOnMouseOver
- if true, stops scrolling when mouse is over the control so user can read message.
Methods of the control are described below.
DisplayMessages
- accepts an OSMessages
collection that you have built external to the control and displays these in order, if message is not ignored.
DisplayMessage
- accepts an OSMessage
object. This is the way you can manually display messages without providing a collection of messages; in essence, the developer can control the display of messages.
NextMessage
- immediately goes to the next un-ignored message.
StopDisplay
- stops scrolling.
If a collection is provided, after the last message is displayed, it starts over again at the beginning. This is where I would update the messages so that the user has the most up-to-date information.
The Code
I won't display all the code here, just some of the interesting parts. Here's my Paint
event:
Private Sub OSNotifier_Paint(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint
Dim bmp As New Bitmap(Me.Width, Me.Height, _
Imaging.PixelFormat.Format24bppRgb)
Dim g As Graphics = Graphics.FromImage(bmp)
g.FillRectangle(New SolidBrush(_BackColor), 0, 0, _bmp.Width, bmp.Height)
Dim PicRectangle As New Rectangle(3, 3, Me.Height - 6, Me.Height - 6)
g.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
Select Case Me._BorderStyle
Case BorderStyle.Fixed3D
Dim p1 As New Pen(SystemColors.ControlLightLight, 1)
Dim P2 As New Pen(SystemColors.ControlDark, 1)
Dim P3 As New Pen(SystemColors.ControlDarkDark, 1)
g.DrawRectangle(p1, New Rectangle(0, 0, Me.Width - 1, Me.Height - 1))
g.DrawRectangle(P2, New Rectangle(0, 0, Me.Width, Me.Height))
g.DrawRectangle(P3, New Rectangle(1, 1, Me.Width - 2, Me.Height - 2))
Case BorderStyle.FixedSingle
Dim p1 As New Pen(SystemColors.ControlLightLight, 1)
g.DrawRectangle(p1, New Rectangle(0, 0, Me.Width - 1, Me.Height - 1))
Case Else
End Select
If Me.DesignMode Then
If Not IsNothing(Me._LowPriorityIcon) Then
g.DrawIcon(Me._LowPriorityIcon, PicRectangle)
ElseIf Not IsNothing(Me._MediumPriorityIcon) Then
g.DrawIcon(Me._MediumPriorityIcon, PicRectangle)
ElseIf Not IsNothing(Me._HighPriorityIcon) Then
g.DrawIcon(Me._HighPriorityIcon, PicRectangle)
End If
e.Graphics.DrawImage(bmp, 0, 0)
Else
If Not IsNothing(_CurrentMessage) Then
Select Case _CurrentMessage.Importance
Case OSMessage.MessageImportanceConstants.osIMPORT_LOW
If Not IsNothing(Me._LowPriorityIcon) Then
g.DrawIcon(Me._LowPriorityIcon, PicRectangle)
End If
Case OSMessage.MessageImportanceConstants.osIMPORT_MEDIUM
If Not IsNothing(Me._MediumPriorityIcon) Then
g.DrawIcon(Me._MediumPriorityIcon, PicRectangle)
End If
Case OSMessage.MessageImportanceConstants.osIMPORT_HIGH
If Not IsNothing(Me._HighPriorityIcon) Then
g.DrawIcon(Me._HighPriorityIcon, PicRectangle)
End If
Case Else
If Me._MessageDisplayType = MessageDisplayTypeConstants.osStatic Then
Me._CurrentLeftPosition = 3
End If
End Select
e.Graphics.DrawImage(bmp, 0, 0)
bmp = New Bitmap(Me.pnlTextArea.Width, Me.pnlTextArea.Height, _
Imaging.PixelFormat.Format24bppRgb)
g = Graphics.FromImage(bmp)
g.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
If Me._MessageDisplayType = MessageDisplayTypeConstants.osFromRightToLeft Then
Me._CurrentLeftPosition -= _ScrollSize
Else
Me._CurrentLeftPosition = 3
End If
If Me._CurrentLeftPosition + g.MeasureString(_CurrentMessage.Text, _
Me.pnlTextArea.Font).Width < 0 Then
If Me._DisplayingMultipleMessages Then
Me._CurrentMessage = GetNextMessage()
RaiseEvent MessageChange(Me._CurrentMessage)
End If
Me._CurrentLeftPosition = Me.pnlTextArea.Width
End If
g.FillRectangle(New SolidBrush(_BackColor), -1, -1, _
bmp.Width + 1, bmp.Height + 1)
If Not IsNothing(Me._CurrentMessage) Then
Dim dTop As Double = (Me.pnlTextArea.Height - _
g.MeasureString(Me._CurrentMessage.Text, _
Me.pnlTextArea.Font).Height) / 2
Dim newPoint As New PointF(Me._CurrentLeftPosition, dTop)
g.DrawString(Me._CurrentMessage.Text, Me.pnlTextArea.Font, _
New SolidBrush(_ForeColor), newPoint)
Me.pnlTextArea.CreateGraphics.DrawImage(bmp, 0, 0)
End If
Else
e.Graphics.DrawImage(bmp, 0, 0)
End If
End If
End Sub
Notice double-buffering of the graphics. .NET does a good job of double buffering using the following style settings for your user control, which I have in my InitializeComponent
method.
Me.SetStyle(ControlStyles.UserPaint, True)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
Me.SetStyle(ControlStyles.DoubleBuffer, True)
I found, with the drawing interrupts so small (it looks good interrupting every 20ms), that I was still getting some flickering, so I employed some old double-buffering techniques (creating a bitmap and then drawing that to the graphics object) which worked out just fine.
Hope you enjoy the control!