使用 GDI+ 的 FrontPage 风格表格选择器






4.95/5 (11投票s)
2004年10月28日
5分钟阅读

65471

1252
使用简单的 GDI+ 渲染技术,在 C# 中创建一个 FrontPage 风格的表格选择器。
引言
Microsoft FrontPage 几乎一直提供一个独特的表格选择器,可以轻松指定创建新表格的行数和列数。这里是 C#/GDI+ 实现的相同基本想法。这应该是学习 GDI+ 的绝佳入门。
背景
在上世纪 90 年代中期到后期,在我学会 HTML 和 WYSIWYG HTML 编辑器问世后,我很快就成为了除了 Notepad 之外最喜欢的网页设计和创作工具 FrontPage。后来,我遇到了一些发誓使用 DreamWeaver 和 Visual InterDev 等优秀工具的网页设计师。这些其他工具确实很优秀。然而,我仍然发现自己会回到 FrontPage 进行设计,原因只有一个:只有 FrontPage 提供了一个合适的表格选择器。我非常依赖那个表格选择器。没有任何更简单的方法可以在新的 3x3 表格的中间单元格中插入一个 2x4 的表格,只需两次鼠标点击,甚至不需要敲击一个键盘。所有其他 WYSIWYG 编辑器都迫使我手动输入行数和列数。为什么?即使到今天,Visual Studio .NET 仍然让我们手动输入如此琐碎的数据,而这完全没有必要!
当我构建 PowerBlog 时,第一版是 VB6,第二版是 C#,我实现了两次 FrontPage 风格的表格选择器,每个编程环境一次。虽然 C# 的实现不是一蹴而就的,但与 VB6 相比,它确实是一项非常简单的任务,在 VB6 中我花了大约两三天的时间才摸索出来。之所以花这么长时间,仅仅是因为这是我第一次涉足 GDI+ 编程。
这里是我为 PowerBlog 应用程序第 2 版实际使用的 TablePicker。
使用代码
TablePicker
类是一个 Windows Forms Form
,应该使用非对话框的 Show()
方法显示。最终的用户选择可以从 SelectedColumns
和 SelectedRows
属性中提取。您应该始终先检查 Cancel
属性是否为 true
。而且,由于 Form
不是使用 ShowDialog()
显示的,您必须手动实现一个睡眠循环,直到 Visible
属性不再是 true
。
这是一个实现示例。(此示例包含在可下载的源代码中。)
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;
}
当然,您需要使用应用程序中的 Location
和/或 Top
/Left
属性来定义您自己的位置设置。
工作原理
GDI+ 使用 Brush
(画刷)和 Pen
(画笔)在 Windows 的 Graphics
“设备”上渲染图形。Brush
和 Pen
的目的几乎相同,只是 Brush
用于填充边框和带有灰度的盒子以及渲染文本,而 Pen
用于绘制盒子的线条。我们使用多个画刷和多个画笔,因为它们有不同的颜色。
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)
当 TablePicker
窗体首次显示时,会触发 Paint()
事件。由于我们已经指定了可见和选定方块的默认数量(SquareQX
、SquareQY
、SelQX
、SelQY
),这些方块会立即被渲染。
渲染过程相当直接。
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);
}
}
}
最后,我们需要检测
- 鼠标在窗体上移动,选择表格尺寸。
- 鼠标快速离开窗体,取消表格尺寸选择。
- 鼠标在窗体上单击,最终确定表格尺寸选择。
- 鼠标在窗体外部单击,取消所有操作。
这些都处理在窗体的事件处理程序中。
/// <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();
}
双缓冲
此时,如果按照上述描述实现代码,结果将是一个功能齐全的表格图片,但由于严重的闪烁会很碍眼。人眼可以看到每一个方块被动态渲染,给人的“感觉”是严重的延迟。虽然我们对加快整体渲染速度无能为力,但我们可以通过使用一种称为双缓冲的技巧来消除闪烁。
双缓冲是指在最终将所有绘制操作的结果从缓冲区传输到实际的 Graphics
“设备”之前,先在缓冲区(例如内存中的位图图像)上进行绘制的过程。
通常,在大多数编程语言中,我们可以像我描述的那样做到这一点——渲染一个位图图像,然后将该位图图像绘制到 Graphics
设备上。然而,GDI+ 和 Windows Forms 提供了一个捷径,可以使用 SetStyle()
方法实现双缓冲。
public TablePicker()
{
// Activates double buffering
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
//
// Required for Windows Form Designer support
//
InitializeComponent();
}
启用双缓冲后,渲染会变得干净利落,没有“弹出”或闪烁,并且最终效果是速度感增强,更不用说正在渲染的方块的可视性也显著提高。
无已知缺陷
据我所知,此 TablePicker
实现是完美的。我可以看到我的实现与 FrontPage 2003 的实现之间唯一的区别是
- 我选择左对齐选中的文本描述。
- 在 FrontPage 的实现中,边框在与按钮相交的左上角有一个间隙。这可以通过在
Paint
事件期间在该位置用控件面颜色进行过度绘制来轻松实现。然而,对于一个通用的、无按钮的 Table Picker 实现,这个间隙是不必要的。 - 颜色有些不同。我更喜欢我的。;) 然而,如果您打算遵循用户当前的 Windows 颜色方案,您可能需要完全使用
SystemColors
enum
。(颜色在Brush
和Pen
声明中指定。)
据我所知,鼠标跟踪、选择或渲染方面没有任何问题。实现很简单,可能也无法再加快多少。但是,如果您知道任何可以改进此设计的方法,请发表评论或 给我发消息。