I am coding using Visual Basic. I have a little problem however.
For my program, I need to layer GIFs/PNGs. I have accomplished this by using .DrawImage() It works fine however, the GIFs are inanimate and static.
I have tried using the ImageAnimator Class, however, I am a bit in the dark. I tried using the sample code from MSDN, and it is not working for me. This is probably because I don't quite understand it.
Private animatedImage As New Bitmap(My.Resources._a_takingnotes)
Private currentlyAnimating As Boolean = False
Public Sub AnimateImage()
If Not currentlyAnimating Then
'Begin the animation only once.
ImageAnimator.Animate(animatedImage, _
New EventHandler(AddressOf Me.OnFrameChanged))
currentlyAnimating = True
End If
End Sub
Private Sub OnFrameChanged(ByVal o As Object, ByVal e As EventArgs)
'Force a call to the Paint event handler.
Me.Invalidate()
End Sub
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
'Begin the animation.
AnimateImage()
'Get the next frame ready for rendering.
ImageAnimator.UpdateFrames()
'Draw the next frame in the animation.
e.Graphics.DrawImage(Me.animatedImage, New Point(0, 0))
PictureBox2.Image = Nothing
PictureBox2.Image = animatedImage
End Sub
The above is the code that is supposed to animate the "(a)takingnotes.gif" (called as a resource).
It returns a static image of the first frame in the PictureBox.
Thank you!
EDIT: AnimateImage() is called on Load, which didn't work. I also tested on every Timer Tick, which also didn't work.
Related
I have two labels in my form which are placed side-by-side to act as one label. When I hover over the labels, I have a function that fades the labels to different colours, which works well. I am trying to apply the MouseHover and MouseLeave event to both labels, so that when I hover over Label1 and move to Label2 (and vice versa), the function doesn't then fade the colour back to the original colour. Currently, moving between the two labels activates MouseLeave followed by MouseHover again in the new label.
I have tried to add both labels to the event trigger, but this hasn't worked. I have also tried placing both labels in a Panel, but that then doesn't trigger the event.
Private Sub fadeHeaderIn(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Label1.MouseHover, Label2.MouseHover
Call fadeLabel("In")
End Sub
Private Sub fadeHeaderOut(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Label1.MouseLeave, Label2.MouseLeave
Call fadeLabel("Out")
End Sub
If the functionality to change the colour of part of the label existed, I'd not need the two labels, so if there is a better way of doing this altogether, I'm happy to do so. Thanks!
I have also tried placing both labels in a Panel, but that then
doesn't trigger the event.
That should work. The panel would act as the boundaries for both labels. You'll get a MouseLeave, though, when you move from the panel to the labels contained within. To prevent a false trigger, simply check if the mouse is still within the bounds of the panel. You can prevent multiple fade ins when moving from label to label by tracking the faded state with a boolean. It'd look something like this:
Public Faded As Boolean = False
Private Sub fadeHeaderIn(sender As Object, e As EventArgs) Handles Label1.MouseHover, Label2.MouseHover
If Not Faded Then
Faded = True
fadeLabel("In")
End If
End Sub
Private Sub fadeHeaderOut(sender As Object, e As EventArgs) Handles Panel1.MouseLeave
If Not Panel1.ClientRectangle.Contains(Panel1.PointToClient(Cursor.Position)) Then
If Faded Then
Faded = False
fadeLabel("Out")
End If
End If
End Sub
Here's a sample of what has been described in comments.
The Text of a control (here, derived from a standard Label) is split in two sections of the same measure. Each section can have a different color.
The active and inactive colors are custom public properties, they can be set in the designer.
Each section is tracked, meaning that the control is aware of what side the Mouse Pointer is currently hovering.
The size of the text is measured using the TextRenderer.MeasureText method. This size is used to calculate the rectangles that include the sections of text.
The Rectangle.Contains([Point]) method is then used to determine which section of the text the mouse pointer in hovering. [Point] is calculated using the MousePosition property, translated to client coordinates using the Control.PointToClient() method.
When the mouse pointer is moved from one section of text to the other (here, just two sections, more could be defined adding more rectangles), the control is Invalidated, causing a call to the OnPaint method of the control.
If the mouse pointer is not hovering a section of text, base.OnPaint(e) is called (also causing the Paint event to raise), which draws the default text with the default color.
In the OnPaint method, the Graphics region is clipped using the Rectangles that define the text sections. A subsequent call to TextRenderer.DrawText, setting the TextFormatFlags.PreserveGraphicsClipping flag, clips the text in the defined region, so just the section of text that fits in the clipping region is painted.
The Graphics.ExcludeClip() method is used here to define these clipping regions.
The TextFormatFlags.ExternalLeading and TextFormatFlags.TextBoxControl are also used to replicate the default text rendering, so the custom text is rendered in the same relative position.
This is how it behaves:
Custom Control Class to test the functionality:
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
<DesignerCategory("Code")>
Public Class LabelSplitText
Inherits Label
Private m_Text As String = String.Empty
Private m_Sections As RectangleF() = Nothing
Private m_PaintText As Boolean = False
ReadOnly flags As TextFormatFlags = TextFormatFlags.ExternalLeading Or
TextFormatFlags.PreserveGraphicsClipping Or
TextFormatFlags.TextBoxControl
Public Sub New()
InitializeComponent()
End Sub
Private Sub InitializeComponent()
ResizeRedraw = True
End Sub
Public ReadOnly Property ActiveRectangle As RectangleF
Public ReadOnly Property ActiveSide As String = String.Empty
Public Property ActiveColor As Color = Color.White
Public Property InactiveColor As Color = Color.DimGray
Protected Overrides Sub OnLayout(e As LayoutEventArgs)
MyBase.OnLayout(e)
Me.AutoSize = False
m_Text = Me.Text
End Sub
Protected Overrides Sub OnMouseEnter(e As EventArgs)
m_Text = Me.Text
Text = String.Empty
m_PaintText = True
MyBase.OnMouseEnter(e)
Invalidate()
End Sub
Protected Overrides Sub OnMouseLeave(e As EventArgs)
m_PaintText = False
Me.Text = m_Text
MyBase.OnMouseLeave(e)
End Sub
Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
MyBase.OnMouseMove(e)
Invalidate()
If m_Sections Is Nothing Then Return
Me._ActiveRectangle = If(m_Sections(0).Contains(e.Location), m_Sections(0), m_Sections(1))
End Sub
Protected Overrides Sub OnMouseClick(e As MouseEventArgs)
Me._ActiveSide = If(m_Sections(0).Contains(e.Location), "left", "right")
MyBase.OnMouseClick(e)
End Sub
Protected Overrides Sub OnPaint(e As PaintEventArgs)
If Not m_PaintText Then
MyBase.OnPaint(e)
Return
End If
Dim textSize As SizeF = TextRenderer.MeasureText(e.Graphics, m_Text, Me.Font, Me.ClientSize, flags)
m_Sections = GetTextAreaSections(textSize)
e.Graphics.ExcludeClip(Rectangle.Round(m_Sections(1)))
TextRenderer.DrawText(e.Graphics, m_Text, Me.Font, Point.Empty, GetSectionColor(0), flags)
e.Graphics.ResetClip()
e.Graphics.ExcludeClip(Rectangle.Round(m_Sections(0)))
TextRenderer.DrawText(e.Graphics, m_Text, Me.Font, Point.Empty, GetSectionColor(1), flags)
End Sub
Private Function GetSectionColor(section As Integer) As Color
Return If(m_Sections(section).Contains(PointToClient(MousePosition)),
Me.ActiveColor, Me.InactiveColor)
End Function
Private Function GetTextAreaSections(textSize As SizeF) As RectangleF()
If textSize.Width > Me.ClientSize.Width Then textSize.Width = Me.ClientSize.Width
Dim rectLeft = New RectangleF(PointF.Empty,
New SizeF(textSize.Width / 2.0F, Me.ClientSize.Height))
Dim rectRight = New RectangleF(New PointF(textSize.Width / 2.0F, 0),
New SizeF(textSize.Width / 2.0F, Me.ClientSize.Height))
Return {rectLeft, rectRight}
End Function
End Class
I am using a worker thread to load .png images (from a path string) into a global PictureBox2 object, and then exit the _RunWorkerCompleted to use the PictureBox2's width and height for additional work in the method called processpic2. Everything works fine until about the 5th or 6th images were added to the PB. A this point, in the processpic2 method, an exception is thrown since the image property of the PictureBox2 evaluates to nothing.
Why would a PB stop taking images after a while?
Public Class Form1
Public WithEvents BackgroundWorker1 As New System.ComponentModel.BackgroundWorker
Private Sub BackGroundworker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim args As ArgumentType = e.Argument
PictureBox2.Image = Nothing
PictureBox2.Invalidate()
Dim img As Image
Using str As Stream = File.OpenRead(args._pathstr)
img = Image.FromStream(str)
End Using
PictureBox2.Image = img
e.Result = "done"
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _Handles BackgroundWorker1.RunWorkerCompleted
'Called when the BackgroundWorker is completed.
processpic2()
End Sub
Sub btnLoadPicture(pathstr)
Dim args As ArgumentType = New ArgumentType()
args._pathstr = pathstr
BackgroundWorker1.RunWorkerAsync(args)
End Sub
Sub processpic2()
If PictureBox2.Image Is Nothing Then MsgBox("Image is Nothing")
End Sub
The whole point of a BackgroundWorker is to do background work. Making changes to the UI is the exact opposite of background work. It is foreground work. If your task is to clear the current contents of a PictureBox, load an image from a file and then display that image, only the middle step is background work so only the middle step should be done in the DoWork event handler. The first step should be done before you call RunWorkerAsync and the last step should be done in the RunWorkerCompleted event handler.
Having said all that, why use a BackgroundWorker at all in this case? Why not simply call the LoadAsync method of the PictureBox itself?
SOLUTION - thanks to the suggestions received, and what I found on MSDN regarding the LoadAsync method of PictureBox the following code solved the issue:
PictureBox2.Image = Nothing
PictureBox2.WaitOnLoad = False
' Load the image asynchronously.
PictureBox2.LoadAsync(pathstr)
Okay, so I am trying to make a program that each time you click (doesn't matter where) a random colored, and sized circle appears where you happened to click. however, the only way I can add a shape is via Paint event. here is the code I have now:
Private Sub Form1_Paint(ByVal Sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint
Using Brush1 As New SolidBrush(Color.Orange)
e.Graphics.FillEllipse(Brush1, MousePosition.X, MousePosition.Y, 100, 100)
End Using
End Sub
I need to know a line of code that I can use in a mouse click event, that will re-run this sub. I know how to change the size, and make it random, I just don't know how to run this sub multiple times, more precisely; run this sub once after each mouse click. If someone can help, I would appreciate it!
Just as Plutonix explained, a refresh is handled by calling the Invalidate method.
The thing you need to remember is that whatever is painted on a surface is not persistent, so you need to redraw the whole screen every time. There are, of course, many ways in which this can be optimized for performance purposes, as this process can be extremely CPU intensive; specially, since GDI+ is not hardware accelerated.
So, what you need to do is:
Record every click (x, y position) and store it
Since the radius of each circle is random, determine the radius when the user clicks the form, then store it along with the x, y position of the click
Then, have the Paint event re-draw each stored sequence of clicks (with their respective radii) and re-draw each circle over and over.
Here's an implementation that will do the trick. Just paste this code inside any Form's class to test it:
Private Class Circle
Public ReadOnly Property Center As Point
Public ReadOnly Property Radius As Integer
Public Sub New(center As Point, radius As Integer)
Me.Center = center
Me.Radius = radius
End Sub
End Class
Private circles As New List(Of Circle)
Private radiusRandomizer As New Random()
Private Sub FormLoad(sender As Object, e As EventArgs) Handles MyBase.Load
Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True) ' Not really necessary in this app...
Me.SetStyle(ControlStyles.OptimizedDoubleBuffer, True)
Me.SetStyle(ControlStyles.ResizeRedraw, True)
Me.SetStyle(ControlStyles.UserPaint, True)
End Sub
Private Sub FormMouseClick(sender As Object, e As MouseEventArgs) Handles Me.MouseClick
circles.Add(New Circle(New Point(e.X, e.Y), radiusRandomizer.Next(10, 100)))
Me.Invalidate()
End Sub
Private Sub FormPaint(sender As Object, e As PaintEventArgs) Handles Me.Paint
Dim g As Graphics = e.Graphics
g.Clear(Color.Black)
Using p As New Pen(Color.White)
For Each c In circles
g.DrawEllipse(p, c.Center.X - c.Radius \ 2, c.Center.Y - c.Radius \ 2, c.Radius, c.Radius)
Next
End Using
End Sub
Here's what you'll get after a few clicks on the form
This is clearly a problem of me not understanding how to properly setup a UI thread, but I can't figure out how to fix it.
I have a datagridview where I click a button, get the information from the network, and then display it on the datagridview with the new data. While it is on the network I have a form I show with an updating gif, a form I called "loading". Within that form I have the gif updating using the typical OnFrameChanged and m_isAnimating code that is on the internet.
However, no matter what format I use, I always get this exception caught here:
Public loader As New Loading
Private Sub OnFrameChanged(ByVal o As Object, ByVal e As EventArgs)
Try ' If animation is allowed call the ImageAnimator UpdateFrames method
' to show the next frame in the animation.
Me.Invalidate()
If m_IsAnimating Then
ImageAnimator.UpdateFrames()
Me.Refresh()
'Draw the next frame in the animation.
Dim aGraphics As Graphics = PictureBox1.CreateGraphics
aGraphics.DrawImage(_AnimatedGif, New Point(0, 0))
aGraphics.Dispose()
End If
Catch ex As InvalidOperationException
End Try
End Sub
And it usually says something along the lines of "was accessed from a thread it wasn't created on" or "Cannot access a disposed object. Object name: 'PictureBox'."
But I don't know why that is, since I am creating a new instance here every time. Here's the button's code:
Private Sub btnSlowSearch_Click(sender As Object, e As EventArgs) Handles btnSlowSearch.Click
Me.Cursor = Cursors.WaitCursor
'get datatable
loader.Show()
BWorkerLoadProp.RunWorkerAsync() 'go get data on network
'bworker will update datagridview with new data
'wait for worker to finish
If BWorkerLoadProp.IsBusy Then
Threading.Thread.Sleep(1)
End If
loader.Close()
End Sub
I realize it isn't very good code, but I have tried putting the loader inside the background worker, I have tried whatever. But no matter what the exception is called.
What's the proper way to show another updating form as I do background work?
The behavior documented is difficult to reproduce.
Probably something between the thread switching causes a call to OnFrameChanged after the call to close in the btnSlowSearch_Click.
In any case logic seems to suggest to call the ImageAnimator.StopAnimate in the close event of the form that shows the animation
So looking at your comment above I would add the following to your animator form
// Not needed
// Public loader As New Loading
Private Sub OnFrameChanged(ByVal o As Object, ByVal e As EventArgs)
Try
Me.Invalidate()
If m_IsAnimating Then
ImageAnimator.UpdateFrames()
Me.Refresh()
'Draw the next frame in the animation.
Dim aGraphics As Graphics = PictureBox1.CreateGraphics
aGraphics.DrawImage(_AnimatedGif, New Point(0, 0))
aGraphics.Dispose()
End If
Catch ex As InvalidOperationException
.. do not leave this empty or remove altogether
End Try
End Sub
Private Sub Form_Closing(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
... if you need to stop the closing you should do it here without stopping the animation
If m_IsAnimating Then
ImageAnimator.StopAnimate(AnimatedGif, _
New EventHandler(AddressOf Me.OnFrameChanged))
m_isAnimating = False
End If
End Sub
This is certainly not the only way to do this but I will provide you the simplest working example in hopes that it will help you to correct your own application.
1) Create a new vb.net windows forms application and add a button (Button1) onto the form.
2) Change the Form1 code to this:
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
If fLoading Is Nothing Then ' can only show one loading screen at a time
Dim oLoadingThread As clsLoadingThread = New clsLoadingThread ' creat new thread
oLoadingThread.ShowWaitScreen() ' show the loading screen
'-----------------------------------------
' your real processing would go here
'-----------------------------------------
For i As Int32 = 0 To 999999
Application.DoEvents()
Next
'-----------------------------------------
oLoadingThread.CloseLoadingScreen() ' we are done processing so close the loading form
oLoadingThread = Nothing ' clear thread variable
End If
End Sub
End Class
Public Class clsLoadingThread
Dim oThread As System.Threading.Thread
Private Delegate Sub CloseLoadingScreenDelegate()
Public Sub ShowWaitScreen()
' create new thread that will open the loading form to ensure animation doesn't pause or stop
oThread = New System.Threading.Thread(AddressOf ShowLoadingForm)
oThread.Start()
End Sub
Private Sub ShowLoadingForm()
Dim fLoading As New frmLoading
fLoading.ShowDialog() ' Show loading form
If fLoading IsNot Nothing Then fLoading.Dispose() : fLoading = Nothing ' loading form should be closed by this point but dispose of it just in case
End Sub
Public Sub CloseLoadingScreen()
If fLoading.InvokeRequired Then
' Since the loading form was created on a seperate thread we need to invoke the thread that created it
fLoading.Invoke(New CloseLoadingScreenDelegate(AddressOf CloseLoadingScreen))
Else
' Now we can close the form
fLoading.Close()
End If
End Sub
End Class
Module Module1
Public fLoading As frmLoading
End Module
3) Add a new form and call it frmLoading. Add a picturebox to the form and set the image to your updating gif.
4) Change the frmLoading code to this:
Public Class frmLoading
Private Sub frmLoading_Load(sender As Object, e As EventArgs) Handles Me.Load
fLoading = Me ' ensure that the global loading form variable is set here so we can use it later
End Sub
Private Sub frmLoading_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
fLoading = Nothing ' clear the global loading form since the form is being disposed
End Sub
End Class
Normally I would add the clsLoadingThread Class and Module1 Module to their own files but it's easier to show the code to you this way.
I'm having trouble creating a .gif be animated in my already created visual studio 2013 project.
Been looking online a bit for the "proper" way to do it. Following this MSDN question: Link I copied some of the code to see how it would work. I've also seen references to using a timer.
Public Not Inheritable Class SplashScreen
Private progressGifPath As String = "C:\Documents\Visual Studio 2013\Projects\CameraFinder\icons" + "\orangeLoading.gif"
Private _AnimatedGif As New Bitmap(progressGifPath)
Private m_IsAnimating As Boolean
Public Property IsAnimating() As Boolean
Get
Return m_IsAnimating
End Get
Set(ByVal value As Boolean)
m_IsAnimating = value
End Set
End Property
Private Sub VICameraFinderLoading_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
'Set up the dialog text at runtime according to the application's assembly information.
If My.Application.Info.Title <> "" Then
Else
End If
End Sub
Private Sub PlayGIF()
If Not m_IsAnimating Then
ImageAnimator.Animate(_AnimatedGif, New EventHandler(AddressOf Me.OnFrameChanged))
m_IsAnimating = True
End If
End Sub
Private Sub OnFrameChanged(ByVal o As Object, ByVal e As EventArgs)
If m_IsAnimating Then
ImageAnimator.UpdateFrames()
Dim aGraphics As Graphics = PictureBox.CreateGraphics
aGraphics.DrawImage(_AnimatedGif, New Point(30, 30))
aGraphics.Dispose()
End If
End Sub
End Class
So I tried this, but I don't see anything on the picture box drawn (I confirmed to see it was brought to the front). And apparently there can be a size limit to the gif included. Any ideas on what would be the better way to include it?
Thanks!
Figured it out. I hadn't set the image of the picture box to the gif.
EDIT:
To clarify, the code above wasn't the problem at all. I actually didn't need it in any way. I removed it from the splash screen class, completely. The solution was to go into my picture box's property (F4) and set the backgroundImage to the .gif's filepath (thus importing it as an embedded resource). Doing so has allowed for animation in the splash screen.