65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (11投票s)

2004年10月28日

5分钟阅读

viewsIcon

65471

downloadIcon

1252

使用简单的 GDI+ 渲染技术,在 C# 中创建一个 FrontPage 风格的表格选择器。

TablePicker in action

引言

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() 方法显示。最终的用户选择可以从 SelectedColumnsSelectedRows 属性中提取。您应该始终先检查 Cancel 属性是否为 true。而且,由于 Form 不是使用 ShowDialog() 显示的,您必须手动实现一个睡眠循环,直到 Visible 属性不再是 true

这是一个实现示例。(此示例包含在可下载的源代码中。)

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;
}

当然,您需要使用应用程序中的 Location 和/或 Top/Left 属性来定义您自己的位置设置。

工作原理

GDI+ 使用 Brush(画刷)和 Pen(画笔)在 Windows 的 Graphics“设备”上渲染图形。BrushPen 的目的几乎相同,只是 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() 事件。由于我们已经指定了可见和选定方块的默认数量(SquareQXSquareQYSelQXSelQY),这些方块会立即被渲染。

渲染过程相当直接。

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“设备”之前,先在缓冲区(例如内存中的位图图像)上进行绘制的过程。

Diagram: Double-Buffering

通常,在大多数编程语言中,我们可以像我描述的那样做到这一点——渲染一个位图图像,然后将该位图图像绘制到 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。(颜色在 BrushPen 声明中指定。)

据我所知,鼠标跟踪、选择或渲染方面没有任何问题。实现很简单,可能也无法再加快多少。但是,如果您知道任何可以改进此设计的方法,请发表评论或 给我发消息

© . All rights reserved.