We have a Windows Forms application that uses the Microsoft InkEdit control. This has not been a problem for 15 years. Worked fine with windows 7, 8 and 10. However, a recent windows 10 update has introduced a memory leak when this control is called.
There was a similar question on Stack Overflow:
[https://stackoverflow.com/questions/63645140/memory-leaks-in-inkedit-control]
We have implemented those solutions into our code however we still have the memory leak.
Analysis seems to indicate the the windows DLL mshwLatin.dll is leaking memory.
I am showing a simple example to duplicate the problem. Again, occurs on somewhat latest Windows 10. Have a window call this window and then close it. My sample app starts at 4MB and grows to over 100MB after opening and closing 7-10 times.
Looking for a solution to the memory leak if there is one or how best to report this to Microsoft.
Imports Microsoft.Ink
Public Class Form2
Dim inkfield As New InkEdit
Dim inkfield2 As New InkEdit
Dim inkfield3 As New InkEdit
Dim inkfield4 As New InkEdit
Dim inkfield5 As New InkEdit
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.Controls.Add(inkfield)
Me.Controls.Add(inkfield2)
Me.Controls.Add(inkfield3)
Me.Controls.Add(inkfield4)
Me.Controls.Add(inkfield5)
inkfield.Location = New Point(10, 10)
inkfield2.Location = New Point(10, 40)
inkfield3.Location = New Point(10, 70)
inkfield4.Location = New Point(10, 100)
inkfield5.Location = New Point(10, 150)
inkfield.Height = 24
inkfield2.Height = 24
inkfield3.Height = 24
inkfield4.Height = 24
inkfield5.Height = 24
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Me.Close()
End Sub
End Class
I don't know if this will work or not for you, but you can try it, not in the same situation but it worked for me and I still use it. This reduce the size of memory used by the application. It's easy to create and test.
create a class with name clsMemory and insert this code:
Imports System.Runtime.InteropServices
Public Class clsMemory
<DllImport("KERNEL32.DLL", EntryPoint:="SetProcessWorkingSetSize", SetLastError:=True, CallingConvention:=CallingConvention.StdCall)> _
Friend Shared Function SetProcessWorkingSetSize(ByVal pProcess As IntPtr, ByVal dwMinimumWorkingSetSize As Integer, ByVal dwMaximumWorkingSetSize As Integer) As Boolean
End Function
<DllImport("KERNEL32.DLL", EntryPoint:="GetCurrentProcess", SetLastError:=True, CallingConvention:=CallingConvention.StdCall)>
Friend Shared Function GetCurrentProcess() As IntPtr
End Function
Public Sub New()
Dim pHandle As IntPtr = GetCurrentProcess()
SetProcessWorkingSetSize(pHandle, -1, -1)
End Sub
End Class
Create a button btnCleanMemory on your form and insert this code:
Dim MemClass As New clsMemory()
MemClass = Nothing
It's a long shot but just click on that button when there is a huge amount of memory used by the application to see if it will work for you.
Related
I'm downloading files .mp3 and my goal is not to have even a minimum GUI freezing during downloading.
My aim is also to display the bites received in a progress bar and through labels.
This code is working, but sometimes is freezing without any reason, sometimes the progress bar doesn't work until file is completely done.
So far, this is the "best" code I found online for a completely working progress bar during a download, but still gets problems.
How do you think I can increase performances? How can I make a resistant and reliable working progressbar? How can I download also large file without GUI freezing? I tried (curiosity) to download a 600 mb file and it completely freeze, not responding and not giving any issue back.
Thanks
EDIT1: I'm trying with this,eventhough I'm lost on high waves.. Any idea on how can I use this code and insert it into Jimi Answer? Answer
Imports System.IO
Imports System.IO.Path
Imports System.Net
Public Class Form1
Private downloader As MyDownloader = Nothing
Private Sub btnStartDownload_Click(sender As Object, e As EventArgs) Handles btnStartDownload.Click
Dim progress = New Progress(Of String)(
Sub(data)
MsgBox("we are on the UI thread here")
End Sub)
Dim url As Uri = New Uri(TextBox1.Text)
downloader = New MyDownloader()
'How can I remove this second? I don't need download from url every 1 second.
downloader.StartDownload(progress, url, 1)
End Sub
And
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Net
Imports System.Net.Http
Imports System.Text.RegularExpressions
Imports System.Threading
Public Class MyDownloader
Private Shared ReadOnly client As New HttpClient()
client.DownloadProgressChanged += AddressOf Client_DownloadProgressChanged
client.DownloadFileCompleted += AddressOf Client_DownloadFileCompleted
Private interval As Integer = 0
Private Sub Client_DownloadFileCompleted(ByVal sender As Object, ByVal e As AsyncCompletedEventArgs)
System.Windows.Forms.MessageBox.Show("Download OK!", "Message", MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub
Public Sub StartDownload(progress As IProgress(Of String), url As Uri, intervalSeconds As Integer)
interval = intervalSeconds * 1000
Task.Run(Function() DownloadAsync(progress, url))
End Sub
Private Sub Client_DownloadProgressChanged(ByVal sender As Object, ByVal e As DownloadProgressChangedEventArgs)
ProgressBar1.Minimum = 0
Dim receive As Double = Double.Parse(e.BytesReceived.ToString())
Dim total As Double = Double.Parse(e.TotalBytesToReceive.ToString())
Dim percentage As Double = receive / total * 100
label2.Text = $"{String.Format("{0:0.##}", percentage)}%"
ProgressBar1.Value = Integer.Parse(Math.Truncate(percentage).ToString())
End Sub
Private Async Function DownloadAsync(progress As IProgress(Of String), url As Uri) As Task
Dim pattern As String = "<(?:[^>=]|='[^']*'|=""[^""]*""|=[^'""][^\s>]*)*>"
Dim downloadTimeWatch As Stopwatch = New Stopwatch()
downloadTimeWatch.Start()
Do
Try
Dim response = Await client.GetAsync(url, HttpCompletionOption.ResponseContentRead)
Dim data = Await response.Content.ReadAsStringAsync()
data = WebUtility.HtmlDecode(Regex.Replace(data, pattern, ""))
progress.Report(data)
Dim delay = interval - CInt(downloadTimeWatch.ElapsedMilliseconds)
Await Task.Delay(If(delay <= 0, 10, delay))
downloadTimeWatch.Restart()
Catch ex As Exception
End Try
Loop
End Function
End Class
I'm Seriously lost on it, I tried to delete cancel download as I am not going to stop any download and I tried also to delete Download from url every 1 second as I just need one time download for every link.
Thanks
I apologize in advance if my question is too long-winded. I looked at the question “How to update data in GUI with messages that are being received by a thread of another class?” and it is very close to what I am trying to do but the answer was not detailed enough to be helpful.
I have converted a VB6 app to VB.NET (VS2013). The main function of the app is to send queries to a Linux server and display the results on the calling form. Since the WinSock control no longer exists, I’ve created a class to handle the functions associated with the TcpClient class. I can successfully connect to the server and send and receive data.
The problem is that I have multiple forms that use this class to send query messages to the server. The server responds with data to be displayed on the calling form. When I try to update a control on a form, I get the error "Cross-thread operation not valid: Control x accessed from a thread other than the thread it was created on." I know I’m supposed to use Control.InvokeRequired along with Control.Invoke in order to update controls on the Main/UI thread, but I can’t find a good, complete example in VB. Also, I have over 50 forms with a variety of controls on each form, I really don’t want to write a delegate handler for each control. I should also mention that the concept of threads and delegates is very new to me. I have been reading everything I can find on this subject for the past week or two, but I’m still stuck!
Is there some way to just switch back to the Main Thread? If not, is there a way I can use Control.Invoke just once to cover a multitude of controls?
I tried starting a thread just after connecting before I start sending and receiving data, but netStream.BeginRead starts its own thread once the callback function fires. I also tried using Read instead of BeginRead. It did not work well if there was a large amount of data in the response, BeginRead handled things better. I feel like Dorothy stuck in Oz, I just want to get home to the main thread!
Thanks in advance for any help you can provide.
Option Explicit On
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading
Friend Class ATISTcpClient
Public Event Receive(ByVal data As String)
Private Shared WithEvents oRlogin As TcpClient
Private netStream As NetworkStream
Private BUFFER_SIZE As Integer = 8192
Private DataBuffer(BUFFER_SIZE) As Byte
Public Sub Connect()
Try
oRlogin = New Net.Sockets.TcpClient
Dim localIP As IPAddress = IPAddress.Parse(myIPAddress)
Dim localPrt As Int16 = myLocalPort
Dim ipLocalEndPoint As New IPEndPoint(localIP, localPrt)
oRlogin = New TcpClient(ipLocalEndPoint)
oRlogin.NoDelay = True
oRlogin.Connect(RemoteHost, RemotePort)
Catch e As ArgumentNullException
Debug.Print("ArgumentNullException: {0}", e)
Catch e As Net.Sockets.SocketException
Debug.Print("SocketException: {0}", e)
End Try
If oRlogin.Connected() Then
netStream = oRlogin.GetStream
If netStream.CanRead Then
netStream.BeginRead(DataBuffer, 0, BUFFER_SIZE, _
AddressOf DataArrival, DataBuffer)
End If
Send(vbNullChar)
Send(User & vbNullChar)
Send(User & vbNullChar)
Send(Term & vbNullChar)
End If
End Sub
Public Sub Send(newData As String)
On Error GoTo send_err
If netStream.CanWrite Then
Dim sendBytes As [Byte]() = Encoding.UTF8.GetBytes(newData)
netStream.Write(sendBytes, 0, sendBytes.Length)
End If
Exit Sub
send_err:
Debug.Print("Error in Send: " & Err.Number & " " & Err.Description)
End Sub
Private Sub DataArrival(ByVal dr As IAsyncResult)
'This is where it switches to a WorkerThread. It never switches back!
On Error GoTo dataArrival_err
Dim myReadBuffer(BUFFER_SIZE) As Byte
Dim myData As String = ""
Dim numberOfBytesRead As Integer = 0
numberOfBytesRead = netStream.EndRead(dr)
myReadBuffer = DataBuffer
myData = myData & Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead)
Do While netStream.DataAvailable
numberOfBytesRead = netStream.Read(myReadBuffer, 0, myReadBuffer.Length)
myData = myData & Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead)
Loop
'Send data back to calling form
RaiseEvent Receive(myData)
'Start reading again in case we don‘t have the entire response yet
If netStream.CanRead Then
netStream.BeginRead(DataBuffer, 0,BUFFER_SIZE,AddressOf DataArrival,DataBuffer)
End If
Exit Sub
dataArrival_err:
Debug.Print("Error in DataArrival: " & err.Number & err.Description)
End Sub
Instead of using delegates one could use anonymous methods.
Singleline:
uicontrol.Window.Invoke(Sub() ...)
Multiline:
uicontrol.Window.Invoke(
Sub()
...
End Sub
)
If you don't want to pass an UI control every time you need to invoke, create a custom application startup object.
Friend NotInheritable Class Program
Private Sub New()
End Sub
Public Shared ReadOnly Property Window() As Form
Get
Return Program.m_window
End Get
End Property
<STAThread()> _
Friend Shared Sub Main()
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Dim window As New Form1()
Program.m_window = window
Application.Run(window)
End Sub
Private Shared m_window As Form
End Class
Now, you'll always have access to the main form of the UI thread.
Friend Class Test
Public Event Message(text As String)
Public Sub Run()
Program.Window.Invoke(Sub() RaiseEvent Message("Hello!"))
End Sub
End Class
In the following sample code, notice that the Asynchronous - Unsafe run will throw a Cross-thread exception.
Imports System.Threading
Imports System.Threading.Tasks
Public Class Form1
Public Sub New()
Me.InitializeComponent()
Me.cbOptions = New ComboBox() With {.TabIndex = 0, .Dock = DockStyle.Top, .DropDownStyle = ComboBoxStyle.DropDownList} : Me.cbOptions.Items.AddRange({"Asynchronous", "Synchronous"}) : Me.cbOptions.SelectedItem = "Asynchronous"
Me.btnRunSafe = New Button() With {.TabIndex = 1, .Dock = DockStyle.Top, .Text = "Run safe!", .Height = 30}
Me.btnRunUnsafe = New Button() With {.TabIndex = 2, .Dock = DockStyle.Top, .Text = "Run unsafe!", .Height = 30}
Me.tbOutput = New RichTextBox() With {.TabIndex = 3, .Dock = DockStyle.Fill}
Me.Controls.AddRange({Me.tbOutput, Me.btnRunUnsafe, Me.btnRunSafe, Me.cbOptions})
Me.testInstance = New Test()
End Sub
Private Sub _ButtonRunSafeClicked(s As Object, e As EventArgs) Handles btnRunSafe.Click
Dim mode As String = CStr(Me.cbOptions.SelectedItem)
If (mode = "Synchronous") Then
Me.testInstance.RunSafe(mode)
Else 'If (mode = "Asynchronous") Then
Task.Factory.StartNew(Sub() Me.testInstance.RunSafe(mode))
End If
End Sub
Private Sub _ButtonRunUnsafeClicked(s As Object, e As EventArgs) Handles btnRunUnsafe.Click
Dim mode As String = CStr(Me.cbOptions.SelectedItem)
If (mode = "Synchronous") Then
Me.testInstance.RunUnsafe(mode)
Else 'If (mode = "Asynchronous") Then
Task.Factory.StartNew(Sub() Me.testInstance.RunUnsafe(mode))
End If
End Sub
Private Sub TestMessageReceived(text As String) Handles testInstance.Message
Me.tbOutput.Text = (text & Environment.NewLine & Me.tbOutput.Text)
End Sub
Private WithEvents btnRunSafe As Button
Private WithEvents btnRunUnsafe As Button
Private WithEvents tbOutput As RichTextBox
Private WithEvents cbOptions As ComboBox
Private WithEvents testInstance As Test
Friend Class Test
Public Event Message(text As String)
Public Sub RunSafe(mode As String)
'Do some work:
Thread.Sleep(2000)
'Notify any listeners:
Program.Window.Invoke(Sub() RaiseEvent Message(String.Format("Safe ({0}) # {1}", mode, Date.Now)))
End Sub
Public Sub RunUnsafe(mode As String)
'Do some work:
Thread.Sleep(2000)
'Notify any listeners:
RaiseEvent Message(String.Format("Unsafe ({0}) # {1}", mode, Date.Now))
End Sub
End Class
End Class
Thank you to those who took the time to make suggestions. I found a solution. Though it may not be the preferred solution, it works beautifully. I simply added MSWINSCK.OCX to my toolbar, and use it as a COM/ActiveX component. The AxMSWinsockLib.AxWinsock control includes a DataArrival event, and it stays in the Main thread when the data arrives.
The most interesting thing is, if you right click on AxMSWinsockLib.DMSWinsockControlEvents_DataArrivalEvent and choose Go To Definition, the object browser shows the functions and delegate subs to handle the asynchronous read and the necessary delegates to handle BeginInvoke, EndInvoke, etc. It appears MicroSoft has already done the hard stuff that I did not have the time or experience to figure out on my own!
So, I've made an iterative Towers of Hanoi algorithm in Visual Basic, that runs in a while loop (recursion is slow in VB). The catch is it compiles okey, it even runs okey when launched through Visual Studio, but when launched though the Debug and Release generated execs the animation stops with the following message:
After a while, I just see all the pieces moved to the destination pole and the message disappears. So its not a crash per say, as the application is still running in the background, its just this message that pops out, ruining the animation. I just want my program to run just as it runs when launched directly from Visual Studio.
After a bit of thinking ...
I'm starting to believe this happens because Win7 treats the fact the application runs in a while loop as unresponsive (7 pieces in Towers of Hanoi ca take a while to rearrange), therefore it tries to close it.
How can I just make my application ignore Window's advertisements ?
I suggest that you do the calculation in the application idle event just like you do when creating a windows game. This way you ensure that the message queue is not blocked.
Public Class Form1
Public Sub New()
Me.InitializeComponent()
AddHandler Application.Idle, AddressOf Me.OnApplicationIdle
End Sub
Private Sub OnApplicationIdle(sender As Object, e As EventArgs)
Static rnd As New Random()
Dim message As MSG = Nothing
Do While (Not PeekMessage(message, IntPtr.Zero, 0, 0, 0))
'...
Me.BackColor = Color.FromArgb(rnd.Next(0, 256), rnd.Next(0, 256), rnd.Next(0, 256))
'...
Loop
End Sub
<DllImport("user32.dll", CharSet:=CharSet.Auto)> _
Friend Shared Function PeekMessage(<[In](), Out()> ByRef msg As MSG, ByVal hwnd As IntPtr, ByVal msgMin As Integer, ByVal msgMax As Integer, ByVal remove As Integer) As Boolean
End Function
<StructLayout(LayoutKind.Sequential)> _
Friend Structure MSG
Public hwnd As IntPtr
Public message As Integer
Public wParam As IntPtr
Public lParam As IntPtr
Public time As Integer
Public pt_x As Integer
Public pt_y As Integer
End Structure
End Class
I found the following class code on a forum. It works great for the gamepad (up, down, left, right) however all the code for the buttons is missing. Can anyone fill in the blanks?
This works:
Private Sub joystick1_Up() Handles joystick1.Up
moveUp()
End Sub
This does not:
Private Sub joystick1_buttonPressed() Handles joystick1.buttonPressed
MsgBox(joystick1.btnValue)
End Sub
because there is no "buttonPressed" event and I have no idea how to write it.
And here's the class:
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Public Class joystick
Inherits NativeWindow
Private parent As Form
Private Const MM_JOY1MOVE As Integer = &H3A0
' Public Event Move(ByVal joystickPosition As Point)
Public btnValue As String
Public Event Up()
Public Event Down()
Public Event Left()
Public Event Right()
<StructLayout(LayoutKind.Explicit)> _
Private Structure JoyPosition
<FieldOffset(0)> _
Public Raw As IntPtr
<FieldOffset(0)> _
Public XPos As UShort
<FieldOffset(2)> _
Public YPos As UShort
End Structure
Private Class NativeMethods
Private Sub New()
End Sub
' This is a "Stub" function - it has no code in its body.
' There is a similarly named function inside a dll that comes with windows called
' winmm.dll.
' The .Net framework will route calls to this function, through to the dll file.
<DllImport("winmm", CallingConvention:=CallingConvention.Winapi, EntryPoint:="joySetCapture", SetLastError:=True)> _
Public Shared Function JoySetCapture(ByVal hwnd As IntPtr, ByVal uJoyID As Integer, ByVal uPeriod As Integer, <MarshalAs(UnmanagedType.Bool)> ByVal changed As Boolean) As Integer
End Function
End Class
Public Sub New(ByVal parent As Form, ByVal joyId As Integer)
AddHandler parent.HandleCreated, AddressOf Me.OnHandleCreated
AddHandler parent.HandleDestroyed, AddressOf Me.OnHandleDestroyed
AssignHandle(parent.Handle)
Me.parent = parent
Dim result As Integer = NativeMethods.JoySetCapture(Me.Handle, joyId, 100, True)
End Sub
Private Sub OnHandleCreated(ByVal sender As Object, ByVal e As EventArgs)
AssignHandle(DirectCast(sender, Form).Handle)
End Sub
Private Sub OnHandleDestroyed(ByVal sender As Object, ByVal e As EventArgs)
ReleaseHandle()
End Sub
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
If m.Msg = MM_JOY1MOVE Then
' Joystick co-ords.
' (0,0) (32768,0) (65535, 0)
'
'
'
' (0, 32768) (32768, 32768) (65535, 32768)
'
'
'
'
' (0, 65535) (32768, 65535) (65535, 65535)
'
Dim p As JoyPosition
p.Raw = m.LParam
' RaiseEvent Move(New Point(p.XPos, p.YPos))
If p.XPos > 16384 AndAlso p.XPos < 49152 Then
' X is near the centre line.
If p.YPos < 6000 Then
' Y is near the top.
RaiseEvent Up()
ElseIf p.YPos > 59536 Then
' Y is near the bottom.
RaiseEvent Down()
End If
Else
If p.YPos > 16384 AndAlso p.YPos < 49152 Then
' Y is near the centre line
If p.XPos < 6000 Then
' X is near the left.
RaiseEvent Left()
ElseIf p.XPos > 59536 Then
' X is near the right
RaiseEvent Right()
End If
End If
End If
End If
If btnValue <> m.WParam.ToString Then
btnValue = m.WParam.ToString
End If
MyBase.WndProc(m)
End Sub
End Class
Instead of using the older winmm, I would use XInput instead (or if you can, use XNA).
There are a couple of ways you could go to do this. One step up is to use the XInput dlls directly as outlined by this question on the MSDN forums. That's still fairly ugly though. Probably the "easier" way to do this is using a wrapper library that exists out there like SlimDX or SharpDX.
One of the advantages of using XInput via SlimDX or SharpDX is that it will also work within a Windows Store app for Windows 8 :).
Here's a snippet from the GamePad sample in SharpDX:
C#
var controllers = new[] { new Controller(UserIndex.One), new Controller(UserIndex.Two), new Controller(UserIndex.Three), new Controller(UserIndex.Four) };
// Get 1st controller available
Controller controller = null;
foreach (var selectControler in controllers)
{
if (selectControler.IsConnected)
{
controller = selectControler;
break;
}
}
VB
Dim controllers As New List(Of Controller)
controllers.Add(New Controller(UserIndex.One))
controllers.Add(New Controller(UserIndex.Two))
controllers.Add(New Controller(UserIndex.Three))
controllers.Add(New Controller(UserIndex.Four))
Dim controller as Controller = Nothing;
For Each selectController In controllers
If selectController.IsConnected Then
controller = selectController
Exit For
End If
Next
Then you can get the state to work with using:
var state = controller.GetState();
You will notice that XInput uses more of a polling model, so you will need to occasionally check for a button press to detect this. If you need to poll continuously, you can probably spin up a new Task to do this on.
VB.NET, .NET 4
Hello,
I am writing an industrial control front-end that has some "fancy" graphics to indicate the states of some machinery. For example, I indicate that some heaters are on by showing red wavy arrows emanating out from a picture of a heater. I accomplished this by creating a class that inherits from PictureBox and using a timer to advance the images:
Public Class AnimatedPictureBox
Inherits PictureBox
Private WithEvents Timer As New Timers.Timer
Public Property Interval As Double
Get
Return Timer.Interval
End Get
Set(ByVal value As Double)
Timer.Interval = value
End Set
End Property
Public ImageList As New List(Of Image)
Private NextImageIndex As Integer = 0
Public Sub New(ByVal Interval As Integer)
MyBase.New()
Timer = New Timers.Timer
Me.Interval = Interval
Me.SizeMode = PictureBoxSizeMode.StretchImage
Me.Visible = True
End Sub
Public Sub New()
Me.New(100)
End Sub
Public Sub BeginAnimation(ByVal [Loop] As Boolean)
Timer.Start()
End Sub
Public Sub BeginAnimation()
BeginAnimation([Loop]:=True)
End Sub
Public Sub StopAnimation()
Timer.Stop()
End Sub
Private Sub Timer_Elapsed(ByVal sender As Object, ByVal e As System.Timers.ElapsedEventArgs) Handles Timer.Elapsed
InvokeControl(Me, _
Sub(x)
x.Image = x.ImageList(NextImageIndex)
x.NextImageIndex += 1
If x.NextImageIndex > x.ImageList.Count - 1 Then x.NextImageIndex = 0
End Sub)
End Sub
End Class
Where InvokeControl is a subroutine in a module that handles cross-thread marshalling of controls:
Private Sub InvokeControl(Of T As Control)(ByVal Control As T, ByVal Action As Action(Of T))
If Control.InvokeRequired Then
Try
Control.Invoke(New Action(Of T, Action(Of T))(AddressOf InvokeControl), New Object() {Control, Action})
Catch ex As Exception
'..Handle the error..
End Try
Else
Action(Control)
End If
End Sub
My question is "Is this an alright way to go about this or is there some obvious better way?" I'm not too good at programming and am worried that having several of these objects with their embedded timers taxing the CPU. Any suggestions would be greatly appreciated!
Thanks in advance,
Brian
P.S. I went with animated GIFs for some other animation stuff, but here, I wanted to be able to handle image formats that can handle a larger palette than a GIF (if that makes sense). In other words, when I tried to save some of my animations as GIFs, the image quality took an unacceptable hit.
I think this is a good approach. The timers give you a lot of flexibility, and like Mike said, they won't take any CPU time to speak of.