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.)
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, Brush
es and Pen
s. Brush
es and Pen
s are nearly identical in purpose, except that Brush
es are used for filling the borders and the boxes with shades of gray and to render text, while the Pen
s 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";
private int DispHeight = 20;
private Font DispFont = new Font("Tahoma", 8.25F);
private int SquareX = 20;
private int SquareY = 20;
private int SquareQX = 3;
private int SquareQY = 3;
private int SelQX = 1;
private int SelQY = 1;
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;
if (SelQX > SquareQX - 1) SquareQX = SelQX + 1;
if (SelQY > SquareQY - 1) SquareQY = SelQY + 1;
this.Width = (SquareX * (SquareQX)) + 5;
this.Height = (SquareY * (SquareQY)) + 6 + DispHeight;
g.DrawRectangle(BorderPen, 0, 0, this.Width - 1, this.Height - 1);
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);
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);
}
}
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.
public bool Cancel {
get {
return bCancel;
}
}
public int SelectedColumns {
get {
return SelQX;
}
}
public int SelectedRows {
get {
return SelQY;
}
}
private void TablePicker_Deactivate(object sender, System.EventArgs e) {
this.Hide();
}
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;
}
if (changed) Invalidate();
}
private void TablePicker_MouseLeave(object sender, System.EventArgs e) {
if (!bHiding) bCancel = true;
this.DialogResult = DialogResult.Cancel;
this.Invalidate();
}
private void TablePicker_MouseEnter(object sender, System.EventArgs e) {
bHiding = false;
bCancel = false;
this.DialogResult = DialogResult.OK;
this.Invalidate();
}
private void TablePicker_Click(object sender, System.EventArgs e) {
bHiding = true;
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".
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()
{
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
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.