从单独的线程绘画?

时间:2013-04-10 13:41:15

标签: .net vb.net multithreading winforms graphics

我正在开发一款使用图形类来创建游戏中所有图片的游戏,我也试图从一个单独的线程中绘制图片以阻止我的主线程冻结。但每次我尝试运行程序时,表单上都没有显示任何内容,我收到此错误...

  

System.NullReferenceException:未将对象引用设置为对象的实例。

     
    

在c:\ users \ samuel \ documents \ visual studio 2012 \ Projects \ Single_Player \ Single_Player \ Form1.vb:第12行中的Single_Player.Form1.mainPaint(PaintEventArgs e)

  

如果我的代码确实有效,我希望它能在屏幕上移动一个椭圆,无论如何这里是我的代码......

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
    Dim paintT As New Threading.Thread(AddressOf mainPaint)
    paintT.Start()
End Sub

Private Sub mainPaint(e As PaintEventArgs)
    e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
    e.Graphics.CompositingQuality = Drawing2D.CompositingQuality.HighQuality
    Dim playerX As Integer = 0
    Dim playerY As Integer = 206
    Do While 0 / 0
        e.Graphics.DrawRectangle(New Pen(Color.FromArgb(128, Color.Black)), 0, 0, 884, 24)
        e.Graphics.FillRectangle(New SolidBrush(Color.FromArgb(128, Color.Black)), 0, 0, 884, 24)
        e.Graphics.DrawString("Life: 5", New Font("DigifaceWide", 20, GraphicsUnit.Pixel), New SolidBrush(Color.FromArgb(191, Color.Green)), 2, 0)
        e.Graphics.DrawString("Score: 0", New Font("DigifaceWide", 20, GraphicsUnit.Pixel), New SolidBrush(Color.FromArgb(191, Color.White)), 100, 0)
        e.Graphics.FillEllipse(New SolidBrush(Color.FromArgb(128, Color.Blue)), playerX, playerY, 24, 24)
        playerX = playerX + 1
        playerY = playerY + 1
        e.Graphics.Clear(Color.Transparent)
        Threading.Thread.Sleep(50)
    Loop
End Sub

更新(再次) 到目前为止我的代码(这次我通过设计窗口添加了一个计时器)......

Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
    Timer1.Interval = 50
    Timer1.Start()
End Sub

Private Sub mainBackgroundPB_Paint(sender As Object, e As PaintEventArgs) Handles Timer1.Tick
    e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
    e.Graphics.CompositingQuality = Drawing2D.CompositingQuality.HighQuality
    e.Graphics.DrawRectangle(New Pen(Color.FromArgb(128, Color.Black)), 0, 0, 884, 24)
    e.Graphics.FillRectangle(New SolidBrush(Color.FromArgb(128, Color.Black)), 0, 0, 884, 24)
    e.Graphics.DrawString("Life: 5", New Font("DigifaceWide", 20, GraphicsUnit.Pixel), New SolidBrush(Color.FromArgb(191, Color.Green)), 2, 0)
    e.Graphics.DrawString("Score: 0", New Font("DigifaceWide", 20, GraphicsUnit.Pixel), New SolidBrush(Color.FromArgb(191, Color.White)), 100, 0)
    e.Graphics.FillEllipse(New SolidBrush(Color.FromArgb(128, Color.Blue)), playerX, playerY, 24, 24)
    playerX = playerX + 1
    playerY = playerY + 1
    e.Graphics.Clear(Color.Transparent)
End Sub

2 个答案:

答案 0 :(得分:2)

你不能在工作线程中绘画,窗口从根本上说是线程不安全的。当计时器滴答时,您的计时器Tick事件处理程序将崩溃并刻录。 Tick事件没有PaintEventArgs参数。

只有Paint事件具有这些参数,添加该事件并移动代码。您可以使用计时器触发绘制,使事件处理程序看起来像这样:

Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
    Me.Invalidate()
End Sub

答案 1 :(得分:0)

上述答案不正确。 CAN 完成,然后 CAN 做得更好。

定时器以预设的间隔运行,这对于可预测性而言是好的,但对性能有害,因为它总是以相同的速度或更慢的速度运行。计时器在游戏中很昂贵,所以如果有的话,请相应地使用它们。

下面是我写的一个快速示例,它在线程中显示绘图,将其传递给委托并使用扫描层将图像写入主UI。

也提到了改进建议。

Public Class Form1

Enum enumGameObjectTypes
    RedBox = 0
    BlueBox = 1
    YellowCircle = 2
End Enum

Structure structGameObjects
    Dim Type As enumGameObjectTypes
    Dim Location As Point
End Structure

' When you have global variables, they are accessible outside the thread.
' You cannot access them on the drawing thread however, without using SyncLock to lock
' the object.
Public GameObjects As New List(Of structGameObjects)
Public tDrawingThread As New System.Threading.Thread(AddressOf DrawingThread)

' We have to use delegates to pass the image
Delegate Sub DrawingDelegateSub(TheDrawnImage As Image)
Public DrawingDelegate As DrawingDelegateSub

' Set this to true when it's time to exit cleanly
Public _ExitThreadSafelyNow As Boolean = False

Public Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
    Dim rand As New Random

    For cnt = 1 To rand.Next(5, 20)
        ' Create some game objects
        Dim newObj As New structGameObjects
        With newObj
            .Type = rand.Next(0, 2)
            .Location = New Point(rand.Next(0, cnt * 100), rand.Next(0, cnt * 50))
        End With
    Next

    ' Set up a delegate (basically a worker that can "move data between threads" safely) 
    DrawingDelegate = AddressOf DrawImage

    ' Start the thread
    tDrawingThread.Start()
End Sub

Public Sub DrawImage(ByVal DrawnImage As Bitmap)

    Dim tmpImage As New Bitmap(1024, 768)

    ' Draw the image to the picture box using an intermediary layer...
    ' The reason we dont just say pb.Image = DrawnImage is because the bitmaps are passed by pointer references NOT just by the image
    ' So, passing it direct would be non-thread safe.
    '
    ' This also isn't the *right* way to do this, but it is just an example.
    '
    ' What we're doing is all the heavy lifting in the thread and then we're scanning pixel by pixel through the image drawn by the thread.
    ' 
    ' The performance here will be awful, so the fix; if you can understand how it works, is to use Bitmap.LockBits;
    ' Performance will ramp up 1000x right away. https://msdn.microsoft.com/en-us/library/5ey6h79d(v=vs.110).aspx
    For x = 1 To DrawnImage.Width - 1
        For y = 1 To DrawnImage.Height - 1
            tmpImage.SetPixel(x, y, DrawnImage.GetPixel(x, y))
        Next
    Next

    pb.Image = tmpImage
    Debug.Print("Wrote frame !")
End Sub

' Main drawing loop
Sub DrawingThread()
    Dim rectBounds As New Rectangle(0, 0, 1024, 768)

    '
    '
    Do Until _ExitThreadSafelyNow = True

        ' Set up the next frame
        Dim ImageBuffer As New Bitmap(rectBounds.Width, rectBounds.Height)
        Dim g As Graphics = Graphics.FromImage(ImageBuffer) ' Create the graphics object
        g.FillRectangle(Brushes.Black, rectBounds)

        ' Lock the gameobjects object until we're done using it.
        SyncLock GameObjects
            For Each obj In GameObjects
                Select Case obj.Type
                    Case enumGameObjectTypes.RedBox
                        g.FillRectangle(Brushes.Red, New Rectangle(obj.Location.X, obj.Location.Y, 15, 15))
                        '
                    Case enumGameObjectTypes.BlueBox
                        g.FillRectangle(Brushes.Blue, New Rectangle(obj.Location.X, obj.Location.Y, 20, 20))
                        '
                    Case enumGameObjectTypes.YellowCircle
                        g.FillEllipse(Brushes.Yellow, New Rectangle(obj.Location.X, obj.Location.Y, 15, 15))
                        '
                    Case Else
                        g.FillEllipse(Brushes.Pink, New Rectangle(obj.Location.X, obj.Location.Y, 15, 15))
                End Select
            Next
        End SyncLock


        '/// All done drawing by this point...

        'TODO: Eventually implement Bitmap.LockBits here!


        SyncLock ImageBuffer
            ' Send the image onto the main thread.
            Call DrawingDelegate(ImageBuffer)
        End SyncLock


    Loop
End Sub

Private Sub Form1_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing

    _ExitThreadSafelyNow = True
End Sub
End Class