Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

FrontPage-Style Table Picker Using GDI+

0.00/5 (No votes)
27 Oct 2004 1  
Create a FrontPage-Style table picker in C# using simple GDI+ rendering techniques.

TablePicker in action

Introduction

Microsoft FrontPage has almost always offered a unique table picker that easily allows you to specify the number of columns and rows for new table creation. Here is a C#/GDI+ implementation of the same basic idea. This should be an excellent introduction to working with GDI+.

Background

Back in the mid-to-late 90's, after I learned HTML and WYSIWYG HTML editors first began to come out, I quickly adopted FrontPage as my favorite web design and authoring tool after Notepad. Later, I came across web designers who swore by other excellent tools such as DreamWeaver and Visual InterDev. These other tools were excellent, indeed. However, I still found myself going back to FrontPage for design, and that was for one reason and one reason alone: only FrontPage offered a proper table picker. I swore by that table picker. There was no easier way to get, say, a new 2x4 table inserted into the middle cell of a new 3x3 table, with only two mouse clicks and not even a single keystroke. All the other WYSIWYG editors forced me to hand-enter the columns and rows. Why? Even to this day, Visual Studio .NET makes us hand-enter such trivial data, when it is so unnecessary!

When I built PowerBlog, first in VB6 for version 1 and then in C# for version 2, I twice implemented the FrontPage-style table picker, once for each programming environment. While the C# effort was not an overnight effort, it did prove to be a very simple task compared to doing it in VB6 that really only took me two or three days to figure out from scratch. And it took me that long only because this was also my first foray into programming with GDI+.

Here, then, is the actual PowerBlog Table Picker that I used in Version 2 of the application.

Using the code

The TablePicker class is a Windows Forms Form that should be displayed using the non-dialog Show() method. The resulting user choice is extracted from the SelectedColumns and SelectedRows properties. You should always check first to be sure that the Cancel property is not true. And since the Form is not shown using ShowDialog(), you must manually implement a sleep loop until the Visible property is no longer true.

Here is an example of implementation. (This example is included in the downloadable source code.)

TablePicker demo app - Pic 1 TablePicker demo app - Pic 2

private void toolBar1_ButtonClick(object sender, 
        System.Windows.Forms.ToolBarButtonClickEventArgs e) {
    Accentra.Controls.TablePicker tp = new Accentra.Controls.TablePicker();
    tp.Location = this.PointToScreen(new Point(0, 0));
    tp.Top += toolBar1.Top + toolBar1.ButtonSize.Height;
    tp.Left += toolBar1.Left;
    tp.Show();
    while (tp.Visible) {
        Application.DoEvents();
        System.Threading.Thread.Sleep(0);
    }
    if (!tp.Cancel) {
        textBox1.Text = tp.SelectedColumns.ToString();
        textBox2.Text = tp.SelectedRows.ToString();
    }
    toolBarButton1.Pushed = false;
}

You will, of course, need to define your own location settings using the Location and/or Top/Left properties in your application.

How It Works

GDI+ renders graphics onto a Windows Graphics "device" using, in our case, Brushes and Pens. Brushes and Pens are nearly identical in purpose, except that Brushes are used for filling the borders and the boxes with shades of gray and to render text, while the Pens are used to draw the lines of the boxes. We use more than one brush and more than one pen because each has a different color.

private Pen BeigePen = new Pen(Color.Beige, 1);
private Brush BeigeBrush = System.Drawing.Brushes.Beige;
private Brush GrayBrush = System.Drawing.Brushes.Gray;
private Brush BlackBrush = System.Drawing.Brushes.Black;
private Brush WhiteBrush = System.Drawing.Brushes.White;
private Pen BorderPen = new Pen(SystemColors.ControlDark);
private Pen BluePen = new Pen(Color.SlateGray, 1);

private string DispText = "Cancel"; // Display text

private int DispHeight = 20;        // Display ("Table 1x1", "Cancel")

private Font DispFont = new Font("Tahoma", 8.25F);
private int SquareX = 20;           // Width of squares 

private int SquareY = 20;           // Height of squares

private int SquareQX = 3;           // Number of visible squares (X)

private int SquareQY = 3;           // Number of visible squares (Y)

private int SelQX = 1;              // Number of selected squares (x)

private int SelQY = 1;              // Number of selected squares (y)

When the TablePicker form is initially displayed, the Paint() event is triggered. Since we've already specified the default number of visible and selected squares (SquareQX, SquareQY, SelQX, SelQY), those squares are immediately rendered.

The rendering process is fairly straightforward.

private void TablePicker_Paint(object sender, 
                   System.Windows.Forms.PaintEventArgs e) {
    Graphics g = e.Graphics;

    // First, increment the number of visible squares if the 

    // number of selected squares is equal to or greater than the

    // number of visible squares.

    if (SelQX > SquareQX - 1) SquareQX = SelQX + 1;
    if (SelQY > SquareQY - 1) SquareQY = SelQY + 1;

    // Second, expand the dimensions of this form according to the 

    // number of visible squares.

    this.Width = (SquareX * (SquareQX)) + 5;
    this.Height = (SquareY * (SquareQY)) + 6 + DispHeight;

    // Draw an outer rectangle for the border.

    g.DrawRectangle(BorderPen, 0, 0, this.Width - 1, this.Height - 1);

    // Draw the text to describe the selection. Note that since

    // the text is left-justified, only the Y (vertical) position

    // is calculated.

    int dispY = ((SquareY - 1) * SquareQY) + SquareQY + 4;
    if (this.Cancel) {
        DispText = "Cancel";
    } else {
        DispText = SelQX.ToString() + " by " + SelQY.ToString() + " Table";
    }
    g.DrawString(DispText, DispFont, BlackBrush, 3, dispY + 2); 

    // Draw each of the squares and fill with the default color.

    for (int x=0; x<SquareQX; x++) {
        for (int y=0; y<SquareQY; y++) {
            g.FillRectangle(WhiteBrush, (x*SquareX) + 3, (y*SquareY) + 3, 
                                               SquareX - 2, SquareY - 2);
            g.DrawRectangle(BorderPen, (x*SquareX) + 3, (y*SquareY) + 3, 
                                               SquareX - 2, SquareY - 2);
        }
    }

    // Go back and paint the squares with selection colors.

    for (int x=0; x<SelQX; x++) {
        for (int y=0; y<SelQY; y++) {
            g.FillRectangle(BeigeBrush, (x*SquareX) + 3, (y*SquareY) + 3, 
                                               SquareX - 2, SquareY - 2);
            g.DrawRectangle(BluePen, (x*SquareX) + 3, (y*SquareY) + 3, 
                                               SquareX - 2, SquareY - 2);
        }
    }
}

Finally, we need to detect:

  • Mouse moves over the form, selecting table dimensions.
  • Mouse flies quickly away from the form, canceling table dimensions.
  • Mouse clicks on the form, finalizing table dimensions selection.
  • Mouse clicks outside of the form, canceling everything.

These are all dealt within the form's event handlers.

/// <summary>

/// Similar to <code><see cref="DialogResult"/> 

/// == <see cref="DialogResult.Cancel"/></code>,

/// but is used as a state value before the form

/// is hidden and cancellation is finalized.

/// </summary>

public bool Cancel {
    get {
        return bCancel;
    }
}

/// <summary>

/// Returns the number of columns, or the horizontal / X count,

/// of the selection.

/// </summary>

public int SelectedColumns {
    get {
        return SelQX;
    }
}

/// <summary>

/// Returns the number of rows, or the vertical / Y count, 

/// of the selection.

/// </summary>

public int SelectedRows {
    get {
        return SelQY;
    }
}

/// <summary>

/// Detect termination. Hides form.

/// </summary>

private void TablePicker_Deactivate(object sender, System.EventArgs e) {

    // bCancel = true 

    // and DialogResult = DialogResult.Cancel 

    // were previously already set in MouseLeave.


    this.Hide();
}

/// <summary>

/// Detects mouse movement. Tracks table dimensions selection.

/// </summary>

private void TablePicker_MouseMove(object sender, 
                System.Windows.Forms.MouseEventArgs e) {
    int sqx = (e.X / SquareX) + 1;
    int sqy = (e.Y / SquareY) + 1;
    bool changed = false;
    if (sqx != SelQX) {
        changed = true;
        SelQX = sqx;
    }
    if (sqy != SelQY) {
        changed = true;
        SelQY = sqy;
    }

    // Ask Windows to call the Paint event again.

    if (changed) Invalidate();
}

/// <summary>

/// Detects mouse sudden exit from the form to indicate 

/// escaped (canceling) state.

/// </summary>

private void TablePicker_MouseLeave(object sender, System.EventArgs e) {
    if (!bHiding) bCancel = true;
    this.DialogResult = DialogResult.Cancel;
    this.Invalidate();
}

/// <summary>

/// Cancels the prior cancellation caused by MouseLeave.

/// </summary>

private void TablePicker_MouseEnter(object sender, System.EventArgs e) {
    bHiding = false;
    bCancel = false;
    this.DialogResult = DialogResult.OK;
    this.Invalidate();
}

/// <summary>

/// Detects that the user made a selection by clicking.

/// </summary>

private void TablePicker_Click(object sender, System.EventArgs e) {
    bHiding = true; // Not the same as Visible == false

                    // because bHiding suggests that the control

                    // is still "active" (not canceled).

    this.Hide();
}

Double Buffering

At this point, if the above code is implemented as described, the result would be a functional table picture, but would be an eyesore due to severe flicker. The human eye would be able to see each and every square being rendered on the fly, and the "feeling" would be a sense of severe lag. While there is little we can do to speed up the rendering process on the whole, we can eliminate the flickering by using a trick called double buffering.

Double buffering is the process of painting to a buffer (like an in-memory bitmap image) before finally transferring the final result of all the drawings that we've applied, from the buffer to the actual Graphics "device".

Diagram: Double-Buffering

Normally, in most programming languages, we could do this just as I have described--render a bitmap image, then paint the bitmap image to the Graphics device. However, GDI+ and Windows Forms provide a shortcut to allow double-buffering to be accomplished internally using the SetStyle() method.

public TablePicker()
{
    // Activates double buffering

    SetStyle(ControlStyles.UserPaint, true);
    SetStyle(ControlStyles.AllPaintingInWmPaint, true);
    SetStyle(ControlStyles.DoubleBuffer, true);

    //

    // Required for Windows Form Designer support

    //

    InitializeComponent();
}

Enabling double-buffering produces a clean rendering, without "popping" or flickering, and the resulting effect is a feeling of increased speed, not to mention significantly increased visibility of the squares being rendered.

No Known Flaws

This implementation of the TablePicker is, as far as I can tell, flawless. The only differences that I can see between my implementation and the FrontPage 2003 implementation are:

  • I chose to left-align the text description of the selection.
  • In the FrontPage implementation, the border has a gap at the top-left area where it intersects with the button. This can be easily accomplished by over-painting with the control face color in that location during the Paint event. However, for a generic, button-less implementation of a Table Picker, this gap is unnecessary.
  • The colors are somewhat different. I like mine better. ;) However, you might consider using the SystemColors enum entirely if you intend to conform to the user's current color scheme in Windows. (The colors are specified in the Brush and Pen declarations.)

To my knowledge, there are no issues with mouse tracking, selections, or rendering. The implementation is straightforward and probably couldn't be sped up much more. However, if you know of a way to improve on this design, please post a comment or send me a line.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here