如何从后台线程绘制位图图像?

时间:2015-04-16 21:17:57

标签: c# .net multithreading sockets graphics

我有一个socket.BeginReceive异步套接字连接,它启动了自己的线程。我收到的大量数据不适合该方法的第一遍,并且无法知道何时完成接收,因为数据是许多重复的xml标记。

当接收到数据时,它将被解析并使用graphics.fromImage(位图)绘制到内存中的位图图像上。为了绘制它,graphics.DrawImage(位图)在我的重写绘制事件中,以允许在调整大小,最小化等时重新绘制表单。

我使用一个Invoke调用来发信号以使表单无效以启动绘制事件,这可能不是正确的方法,这就是我问你们的原因。

我遇到的问题是,在从BeginReceive方法创建的线程以及主UI线程上调用图形对象,同时抛出异常。

问题1:有没有一种好方法可以知道我什么时候收到套接字上的所有数据?请记住,它不会在第一次通过时收到所有数据。如果是这样,我可以以某种方式发出油漆事件。

问题2:我是否应该在某处阻止NON-UI线程以避免跨线程错误,然后调用主线程来绘制表单?如果是这样,那么这个过程有什么好处呢?

这是我在大部分时间里所做的事情:

private void paint_the_image()
{
    Invoke(new Action(() =>
        {
            this.Invalidate();
        }));
}

// the bitmap image is created when the program loads
public void paint_bitmap(int data_type, string text, int x, int y)
{
    if (data_type == 1)
    {
        using( Graphics gr2 = Graphics.FromImage(bitmap_image))
        {
           gr2.FillRectangle(
            Brushes.White, 
            35 + (x * 14), 200 + (y * 18), 14, 18);
           gr2.DrawString(
            text, new Font("Lucida Console", 14), 
            newBrush, 35 + (x * 14), 200 + (y * 18));
        }
    }
}

//paint event
private void main_sockets_Paint(object sender, PaintEventArgs e)
{
    using(Graphics gr2 = Graphics.FromImage(bitmap_image))
    {
       gr2.DrawImage(bitmap_image, 35, 200, 1500, 700);
    }
}

// this is called right after the socket connection is made.
public void wait_for_data()
{
    if (receive_callBack == null)
    {
        receive_callBack = new AsyncCallback(on_data_received);
    }
    async_result = client_socket_connect.BeginReceive(
        socket_data_buffer, 0, socket_data_buffer.Length, 
        SocketFlags.None, receive_callBack, null);        
}

public void on_data_received(IAsyncResult async)
{
    int char_count = 0;
    char_count = client_socket_connect.EndReceive(async);
    char[] chars = new char[char_count + 1];
    System.Text.Decoder decode = System.Text.Encoding.UTF8.GetDecoder();
    int char_length = decode.GetChars(socket_data_buffer, 0, char_count, chars, 0);
     //processing data....
    paint_bitmap(1, txt.ToString(), ex, why);
    wait_for_data();
}

我是异步连接的新手,所以请随意选择它。任何建议都会有所帮助。

1 个答案:

答案 0 :(得分:1)

这么多错误......从哪里开始?

好吧,让我们从您的问题开始,重点关注您认为关注的线程安全问题:

  

我使用Invoke调用来发信号以使表单无效以启动绘制事件,这可能不是正确的方法

实际上,这是在Winforms中处理屏幕更新的预期方法:当某些数据需要在屏幕上反映出来时,会使显示数据的控件无效,让控件使用Paint事件被提出。在许多情况下,控件在内部处理这一切;例如TextBoxLabelListBox等等。您的代码通常会更新控件在内部存储的数据,并且控件会处理屏幕刷新。

在你的情况下,你正在将位图直接绘制到屏幕上(好吧,有点......代码看起来不会起作用,但我可以推断你的意味着什么到做)。因此,正确的方法是更新位图,然后使其显示的控件无效,以便控件的Paint处理可以将位图绘制到屏幕上。

  

有没有一种好方法可以知道我何时收到套接字上的所有数据?

这取决于您的意思"所有数据"。您还没有解释您的应用程序的网络协议,因此我们不知道这意味着什么。在某些情况下,单个TCP连接用于传输多个数据块;在这种情况下,应用程序协议必须定义某种方式来分隔这些块。对于二进制协议,这通常是在实际数据之前的字节计数。对于基于文本的协议,这可能是特殊字符(例如' \ 0',换行符,LF / CR对等),甚至可能是某种结构化数据(例如XML元素)。

在其他情况下,"所有数据"指的是在单个连接中发送的每个最后一个字节。在这种情况下,您只需等待流结束指示,即接收操作以0字节返回值完成。

  

我遇到的问题是,在从BeginReceive方法创建的线程以及主UI线程上调用图形对象,同时抛出异常。

尽我所知,上面是你的主要问题。解决它的最直接的方法是同步两个线程。这意味着可以在网络I / O代码执行时暂时阻止UI线程,但是网络I / O代码应该能够足够快地运行并且足够短暂的时间,这不会成为问题。

同步可能如下所示:

private readonly object _lock = new object();

// the bitmap image is created when the program loads
public void paint_bitmap(int data_type, string text, int x, int y)
{
    if (data_type == 1)
    {
        lock (_lock)
        {
            using( Graphics gr2 = Graphics.FromImage(bitmap_image))
            {
               gr2.FillRectangle(
                Brushes.White, 
                35 + (x * 14), 200 + (y * 18), 14, 18);
               gr2.DrawString(
                text, new Font("Lucida Console", 14), 
                newBrush, 35 + (x * 14), 200 + (y * 18));
            }
        }
    }
}

//paint event
private void main_sockets_Paint(object sender, PaintEventArgs e)
{
    lock (_lock)
    {
       e.Graphics.DrawImage(bitmap_image, 35, 200, 1500, 700);
    }
}

(我不知道我对Paint事件处理程序的更改是否完全正确,但它肯定比您之前的代码更正确,您只需绘制位图&#39 ;由于某种原因,内容为自己。)

上面使用_lock对象来同步对bitmap_image对象的访问,确保一次只有一个线程实际使用它。


<一旁> 关于您提供的代码示例的一些要点:示例中没有实际调用paint_the_image(),也没有显示txt变量的声明或初始化。 exwhy变量(方式名称很差)也未显示,但这似乎不太重要。

底线:这个示例远不是<{>>每个好的Stack Overflow问题的一部分的good, minimal, complete code example,但鉴于省略了这些密钥,它特别差细节。如果你觉得你没有得到你需要的帮助,你应该花点时间来清理你的问题,以便a)更专注(即一次处理一个问题,并确保你让那部分干净利落地工作)在继续讨论下一期之前),和b)包括所需的MCVE。 的&LT; /预留&GT;


现在,关于这段代码的一些其他想法:

  1. 上面同步的替代方法是双重缓冲(甚至三重缓冲)渲染。您似乎每次都清除位图(顺便说一句,您可以使用Graphics.Clear()方法),因此您不需要将图像从一帧保留到下一帧。
  2. 所以你可以保留两个Bitmap个对象并交替绘制它们。您仍然需要同步,但这可以在绘制时以UI线程锁定的形式完成,并且仅在交换缓冲区时锁定网络I / O代码。由于交换缓冲区数量只需交换引用甚至数组中的标志或索引(取决于您如何实现缓冲区&#34;链&#34;),因此可以保证快速并且永远不会延迟UI任何重要的时间都在线程。

    例如:

    private readonly object _lock = new object();
    private bool _useBufferB;
    
    // the bitmap image is created when the program loads
    public void paint_bitmap(int data_type, string text, int x, int y)
    {
        if (data_type == 1)
        {
            // Note that this test is reversed from the Paint handler one
            using( Graphics gr2 =
                Graphics.FromImage(_useBufferB ? bitmap_image : bitmap_imageB))
            {
               gr2.FillRectangle(
                Brushes.White, 
                35 + (x * 14), 200 + (y * 18), 14, 18);
               gr2.DrawString(
                text, new Font("Lucida Console", 14), 
                newBrush, 35 + (x * 14), 200 + (y * 18));
            }
    
            lock (_lock)
            {
                _useBufferB = !_useBufferB;
            }
        }
    }
    
    //paint event
    private void main_sockets_Paint(object sender, PaintEventArgs e)
    {
        lock (_lock)
        {
           e.Graphics.DrawImage(
               _useBufferB ? bitmap_imageB : bitmap_image, 35, 200, 1500, 700);
        }
    }
    
    1. 接收数据时遇到多个问题。
    2. 以下代码存在问题,原因如下:

      int char_count = 0;
      char_count = client_socket_connect.EndReceive(async);
      char[] chars = new char[char_count + 1];
      

      首先,如果您要在下一个语句中为其分配新值,则没有理由将char_count初始化为0。但更有问题的是,除非您使用单字节字符编码(例如ASCII),否则没有理由期望EndReceive()返回的字节数对应于字符数

      这已经够糟了,但是你有了这个:

      System.Text.Decoder decode = System.Text.Encoding.UTF8.GetDecoder();
      int char_length = decode.GetChars(socket_data_buffer, 0, char_count, chars, 0);
      

      使用Decoder对象可能是件好事。除非你做错了。使用Decoder而不是仅调用Encoding.GetString()或类似的整点是Decoder对象有一个内部缓冲区来存储不完整的字符数据,因此在后续接收时它可以在哪里拾取它停止并确保正确处理在接收操作中分割的多字节字符。当您丢弃Decoder对象并为每个接收操作创建一个全新对象时,您也会丢弃此内部缓冲区并否定使用Decoder的好处。

      不要这样做。

      更好的可能是这样的:

      private readonly Decoder _decoder = new Encoding.UTF8.GetDecoder();
      
      public void on_data_received(IAsyncResult async)
      {
          int byte_count = client_socket_connect.EndReceive(async);
          char[] chars = new char[_decoder.GetCharCount(socket_data_buffer, 0, byte_count)];
      
          _decoder.GetChars(socket_data_buffer, 0, byte_count, chars, 0);
      
           //processing data....
          paint_bitmap(1, new string(chars), ex, why);
          wait_for_data();
      }
      

      虽然没有一个好的代码示例,但是不可能确切地知道这是正确的事情(即你真的想直接将解码后的文本传递给paint_bitmap()吗?方法,或者您打算先进一步处理该文本?)。

      1. 当然,在某些时候,你肯定想要引入对paint_the_image()方法的调用。毕竟,没有它,UI线程不知道重绘任何东西。

      2. 也许这个评论应该是第一个......我不知道。我只是想把所有上述内容排除在外,因为如果你想在以后使用这些技术,你还是需要知道这些内容。

      3. 但是,真的:我想知道你为什么要使用Bitmap这一切。你似乎正在做的就是将文本数据复制到位图,Winforms确实有各种其他文本友好的控件可以用来做到这一点。即使是一个简单的TextBox(设置Multilinetrue来记录多行,如果您希望使用不能修改竞争,请将ReadOnly设置为true ),或ListBox,或RichTextBox,或Label,或......

        有很多选择,所有选择都比处理你自己的位图缓冲区更容易,也可能更有效。