Sunday 3 September 2023

Simple List Control - Part 2

Overview

In my previous post I detailed the steps required to create WinForms project that will eventually house a simple list control.

This post discusses how to implement basic row painting. A brief overview of the Windows painting mechanism is included.

In this post...

Top

Basic Winforms Painting Mechanics

Typically, all painting in a WinForms control is performed by responding to the Paint event. In a derived control overriding the OnPaint method achieves the same result. ALL painting should occur within the OnPaint method. Whilst creating a graphics context within a control is possible, it is generally frowned upon.

Windows raises a paint event whenever a control is resized, or a portion is uncovered, say, after moving another window over the control. It is also possible to cause Windows to raise the paint event manually, by calling the control's Invalidate method. Calling Invalidate will force an entire client area repaint. It is also possible to force a smaller area to be repainted by passing a rectangle to the Invalidate method.

A control conists of zero or more non-client areas and a client areas. Non-client areas include items such as borders, scroll bars and so on. Certain controls perform custom non-client painting. One such example is the list view control that draws column header cells within the non-client area. For our simple control we will only be painting within the client area.

Figure 1 - Client and non-client areas

For the simple list control, we will simply paint all available rows whenever a paint request is received. Whilst this is a somewhat naive approach, it serves as a decent starting point.

Top

Generating Row Data

Switch to the solution tab and select the TestView.cs item. Right-click, then select View Code (or press F7) to view the auto-generated code. You should see something like the following (I removed unused usings for brevity).

using System.Windows.Forms;
using System.Drawing;

namespace Gui
{
  public partial class TestView : UserControl
  {
    public TestView()
    {
      InitializeComponent();
    }
  }
}

Before we can paint any rows, we need some data. To keep things simple, we can add a class method to add sample rows. I will call this new method, AppendSampleRows. We can then override the OnLoad method to call this method and add rows. We also need to maintain a reference to the rows within the control. A list of strings will suffice for now. Let's call this class member variable, _rows. The following code illustrates the idea.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace Gui
{
  public partial class TestView : UserControl
  {
    private List<string> _rows = new List<string>();
    
    public TestView()
    {
      InitializeComponent();
    }

    public static void AppendSampleRows(int count)
    {
      for (int i=0; i<count; i++)
        AppendRow($"Row {i}");
    }

    protected override void OnLoad(EventArgs e)
    {
      _rowData = GenerateRowData(30);
    }    
  }
}

Code walkthrough...

1. Introduce a class member variable, _rows, this will maintain a list of rows to be painted.

2. Introduce a class method, AppendSampleRows, to add sample row data.

3. Override the OnLoad method and call the AppendSampleRows method.

Top

Painting Rows

Now, we can finally override OnPaint to paint the rows. Modify the code so that it looks like the following...

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Windows.Forms;

namespace Gui
{
  public partial class TestView : UserControl
  {
    private IEnumerable<string> _rowData = new string[0];
    
    public TestView()
    {
      InitializeComponent();
    }

    public static void AppendSampleRows(int count)
    {
      for (int i=0; i<count; i++)
        AppendRow($"Row {i}");
    }

    protected override void OnLoad(EventArgs e)
    {
      _rowData = GenerateRowData(30);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
      Graphics g = e.Graphics;
      Rectangle rcRow = ClientRectangle;
      int rowHeight = Font.Height;
      rcRow.Height = rowHeight;

      foreach (string rowText in _rowData)
      {
        g.DrawString(rowText, Font, Brushes.Black, rcRow);
        rcRow.Y += rowHeight;
      }
    }
  }
}

Build the project and run it (F5 for debug run, or CTRL + F5). You should see the following...

Figure 2 - First Run

Try resizing the window. You will notice there are no scroll bars. To view all rows, you essentially have to maximise the window. Still, it is about small incremental steps, learning as you go along. You can also modify the count in the GenerateRowData method. Don't expect any surprises, still, it can be useful to experiment.

Top

Code Walkthrough

So, at this point we have a simple control that can display rows of data. Let's do a code walk through.

1. We added a _rows variable that maintains a list of rows to be painted.

2. We added an AppendSampleRows method, which expects a count parameter. This method simply generates strings that our control can display. We generate text that results in row{N}, where N is the row number, this will prove useful for debugging purposes.

3. The OnPaint method was overridden to draw the individual rows. The implementation can be broken down as follows...

  • Create a temp variable 'g', to hold a reference to the graphics context, saves subsequent typing.
  • Drawing occurs within the client area, rcRow will track each row bounding rectangle as we proceed to draw rows.
  • We use the control's font height to specify the row height. Simplistic, but will suffice for a starting point.
  • As each row is of a fixed height, we set the initial height and can simplay walk through each row, adding the row height after each row.
Top

No comments:

Post a Comment