bonCodigo
bonCodigo

Reputation: 14361

Drawing real time 2D graphics using WinForm

A table called floorLayout has the original cordinates of the objects stored. These details are showed in a 2D picturebox with basic 2D shapes. A slave of above table gets updated real time for new cordinates of the objects and these new locations should be also udpated into the 2D shapes (by changing colour, location etc).

I am certainly new to graphics. So following are couple of questions I would like to clarify.

  1. Do I need a backgroundworker with a thread to handle updates to 2D graphics?
  2. Is there any other approach for this scenario?

Edit after helpful comments:

There's a table with a basic seating plan details. Seat Number, Seat Type (represented by an eclipse or a square), Original Seat Location

When a seat is occupied or reserved, the reference shape in the picture box colour must be changed.

Seats can be in different sections. However at times a certain seat can be coupled with another seat. When a seat is coupled with another, its current location becomes the location of its couple seat (location remain original). Both seats' colour changes.

When decouple, the secondary seat location changes back to its original location and colour changes.

It means for each DML transaction that seating lsyout has an impact. Thats what I want to manage without compromising performance.

The application has three parts. Set up (login is a part of set up), Seating allocation, Graphical layout. Although it is in C#, the model is in 3-tier layered architecture for future web extensibility (if required). Plus having services and data access separately gives lots of freedom n easy to manage.

Given this scenario what're my options?

Edit2:[2014/07/01]

While trying out the Bitmap based drawing for my original question, I have come across an issue related to my threading. I am posting here, as it's infact related to the discussion I had with the capable answerers.

    public void bgWorker_DoWork(object sender, DoWorkEventArgs d)
    {
        //insert seats (sql call via service/access interaces) to table
         AddSeats();
    }

    //this doesn't get fired----<<<-------
    public void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs p)
    {
        txtFlag.Text = p.ProgressPercentage.ToString();

    }

    //this works fine
    public void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs r)
    {
        if (r.Cancelled)
        {
            txtFlag.Text = "Failed";
        }
        else
        {
            //load datagridview using datatable returned by select query
            GetSeats();
            //draw the bitmap with a loop over the datagridview  
            DrawFuntion();

            //bgWorker.ReportProgress(prog, txtFlag.Text);
        }
    }

One major issue: 1. does this really make sense that I use bgworker to do the insertion to database, when it's completed I am calling loadgridview and draw methods? 2. I infact think, it's best to call draw method within bgworker, but I can't figure out how (logically, and functional flow-wise) 3. When I tried to run DrawFunction() within DoWork(), it just threw the biggest cross thread error: that access to UI controls in the form which are created using a different thread is not allowed.

How can I make sense of this?

Upvotes: 2

Views: 4457

Answers (1)

TaW
TaW

Reputation: 54453

Here is the layout I would go for, considering the results of our chat:

Given the time constraint of the current project, keep it in Winforms, but keep WPF in mind for a future revision.

Drawing a thousand seats is not a big problem, but in order to keep the GUI responsive it should be done by a background worker like this:

Create two Bitmap properties:

public Bitmap bmp_Display { get; set; }
public Bitmap bmp_Buffer { get; set; }

In the display Panel's Paint event you simply dump the bmp_Display onto the panel like this:

e.Graphics.DrawImage(bmp_Display,  Point.Empty);

This is a single command and will happen real fast.

To create the updated floorplan the background thread draws the seats onto the bmp_Buffer, maybe like this:

 foreach (Seat s in SeatList)
 {
    e.Graphics.FillRectangle(seatBrushes[s.State], 
                             new Rectangle(s.Location, s.Size);
    e.Graphics.DrawString(s.Name, seatFont, Brushes.Black, s.Location);                        
 }

When it is done, it dumps it onto the bmp_Display like this:

bmp_Display = (Bitmap)bmp_Buffer.Clone();

To do that you should assure thread safety, maybe by a lock.

Finally the display Panel is invalidated.

The details will depend on the data structure and the business logic you will be using. If you will transfer only the changes, use them to update the data structure and still draw them all. You can Initialize the buffer Bitmap with a nice image of the floorplan with the tables and other things.

If needed you can create a helper app to act as a floorplan editor which would create the seats data structure and map it to the floorplan coordinates of the seats.

Here is an example of how the background worker can update the bitmap. Note that the error handling is practically non-existent; also only one of the locks should be necessary. And you need to get at the data somehow. The Seat class is just a dummy, too.

List<SolidBrush> seatBrushes = new List<SolidBrush>() 
    { (SolidBrush)Brushes.Red, (SolidBrush)Brushes.Green  /*..*/ };

public Bitmap bmp_Display { get; set; }
public Bitmap bmp_Buffer { get; set; }

public class Seat  
{
    public string Name { get; set; }
    public int State { get; set; }
    public Point Location { get; set; } 
    //...
}

private void drawFloorplan(List<Seat> seats)
{
    Graphics G = Graphics.FromImage(bmp_Buffer);
    Font sFont = new Font("Consolas", 8f);
    Size seatSize = new Size(32, 20);
    foreach (Seat s in seats)
    {
        G.FillRectangle(seatBrushes[s.State], new Rectangle(s.Location, seatSize));
        G.DrawString(s.Name, sFont, Brushes.Black, s.Location);
    }
    G.Dispose();
    sFont.Dispose();


}

private void bw_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;

        if ((worker.CancellationPending == true))
        {
            e.Cancel = true;
        }
        else
        {
            // get the seat data..
            List<Seat> seats = new List<Seat>();

            if (seats.Count > 0)
            {
                drawFloorplan(seats);
                try { bmp_Display = (Bitmap)bmp_Buffer.Clone(); } 
                catch { /*!!just for testing!!*/ }
                //lock(bmp_Display) { bmp = (Bitmap) bmp_Buffer.Clone(); }
            }

        }
    }

private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if ((e.Cancelled == true))
    { this.tbProgress.Text += "Cancelled!"; }
    else if (!(e.Error == null))
    { this.tbProgress.Text += ("Error: " + e.Error.Message); }
    else
    { panel1.Invalidate(); }
}


private void panel1_Paint(object sender, PaintEventArgs e)
{
    try { e.Graphics.DrawImage(bmp, Point.Empty); } catch {/*!!just for testing!!*/ }
    //lock (bmp){   e.Graphics.DrawImage(bmp, Point.Empty);    }

}

Edit 2 A few words about thread safety: The point of TS is to ensure that no two threads try to access the same object at the same time. Looking at the code one could wonder how it could happen, since it is only upon its completion that the BW thread will trigger the Invalidate. However things are more complicated with threads. They always are! Here for example the problems will come in when the system intself triggers an invalidate at a time the BW thread is dumping its result into the display bitmap. (The system is free to do so, whenever it sees a reason to get the screen updated.)

To avoid this one of the concurring threads should block its access while it is working with the resource. I suggest the do_Work method. Chances are that you never encounter a problem in your tests. My testbed ran up to 30 times per second (!) and after a few minutes it did. The try - catch only prevents crashing. For production code the whole situation should be avoided by using a lock.

Careful with lock though: 'lock blocks' and can create deadlocks, when used too generously..

Upvotes: 2

Related Questions